diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index a95e0e319e..09edf71af8 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -187,7 +187,7 @@ export default class LayerConfig { const keys = Array.from(SharedTagRenderings.SharedTagRendering.keys()) - throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${(keys.join(", "))}`; + throw `Predefined tagRendering ${renderingJson} not found in ${context}.\n Try one of ${(keys.join(", "))}\n If you intent to output this text literally, use {\"render\": } instead"}`; } return new TagRenderingConfig(renderingJson, self.source.osmTags, `${context}.tagrendering[${i}]`); }); @@ -343,7 +343,7 @@ export default class LayerConfig { const iconW = num(iconSize[0]); let iconH = num(iconSize[1]); - const mode = iconSize[2] ?? "center" + const mode = iconSize[2]?.trim()?.toLowerCase() ?? "center" let anchorW = iconW / 2; let anchorH = iconH / 2; @@ -365,12 +365,6 @@ export default class LayerConfig { const self = this; const mappedHtml = tags.map(tgs => { function genHtmlFromString(sourcePart: string): BaseUIElement { - if (sourcePart.indexOf("html:") == 0) { - // We use § as a replacement for ; - const html = sourcePart.substring("html:".length) - const inner = new FixedUiElement(SubstitutingTag.substituteString(html, tgs)).SetClass("block w-min text-center") - return new Combine([inner]).SetClass("flex flex-col items-center"); - } const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0; left: 0`; let html: BaseUIElement = new FixedUiElement(``); @@ -432,7 +426,7 @@ export default class LayerConfig { try { const label = self.label?.GetRenderValue(tgs)?.Subs(tgs) - ?.SetClass("block w-min text-center") + ?.SetClass("block text-center") ?.SetStyle("margin-top: " + (iconH + 2) + "px") if (label !== undefined) { htmlParts.push(new Combine([label]).SetClass("flex flex-col items-center")) diff --git a/Docs/Tools/csvGrapher.py b/Docs/Tools/csvGrapher.py index f4c090c03c..ddd826ed21 100644 --- a/Docs/Tools/csvGrapher.py +++ b/Docs/Tools/csvGrapher.py @@ -424,13 +424,13 @@ def clean_input(contents): yield row -def contributor_count(stats): +def contributor_count(stats, index=1, item = "contributor"): seen_contributors = set() for line in stats: - contributor = line[1] + contributor = line[index] if(contributor in seen_contributors): continue - print("New contributor " + str(len(seen_contributors) + 1) + ": "+contributor) + print("New " + item + " " + str(len(seen_contributors) + 1) + ": "+contributor) seen_contributors.add(contributor) print(line) @@ -440,10 +440,10 @@ def main(): stats = list(clean_input(csv.reader(csvfile, delimiter=',', quotechar='"'))) print("Found " + str(len(stats)) + " changesets") - contributor_count(stats) - create_graphs(stats) - create_per_theme_graphs(stats, 15) - create_per_contributor_graphs(stats, 25) + contributor_count(stats, 3, "theme") + # create_graphs(stats) + # create_per_theme_graphs(stats, 15) + # create_per_contributor_graphs(stats, 25) print("All done!") diff --git a/InitUiElements.ts b/InitUiElements.ts index a5b738e9ca..65a4485e06 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -352,6 +352,11 @@ export class InitUiElements { State.state.backgroundLayer = State.state.backgroundLayerId .map((selectedId: string) => { + if(selectedId === undefined){ + return AvailableBaseLayers.osmCarto + } + + const available = State.state.availableBackgroundLayers.data; for (const layer of available) { if (layer.id === selectedId) { diff --git a/Logic/Actors/AvailableBaseLayers.ts b/Logic/Actors/AvailableBaseLayers.ts index e9855a1cb8..1a025d5a02 100644 --- a/Logic/Actors/AvailableBaseLayers.ts +++ b/Logic/Actors/AvailableBaseLayers.ts @@ -14,18 +14,19 @@ import {Utils} from "../../Utils"; export default class AvailableBaseLayers { - public static osmCarto: BaseLayer = + public static osmCarto: BaseLayer = { id: "osm", - name: "OpenStreetMap", - layer: AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", - "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", - 19, - false, false), + name: "OpenStreetMap", + layer: () => AvailableBaseLayers.CreateBackgroundLayer("osm", "OpenStreetMap", + "https://tile.openstreetmap.org/{z}/{x}/{y}.png", "OpenStreetMap", "https://openStreetMap.org/copyright", + 19, + false, false), feature: null, max_zoom: 19, min_zoom: 0 - } + } + public static layerOverview = AvailableBaseLayers.LoadRasterIndex().concat(AvailableBaseLayers.LoadProviderIndex()); @@ -123,7 +124,7 @@ export default class AvailableBaseLayers { continue } - const leafletLayer = AvailableBaseLayers.CreateBackgroundLayer( + const leafletLayer: () => TileLayer = () => AvailableBaseLayers.CreateBackgroundLayer( props.id, props.name, props.url, @@ -150,10 +151,10 @@ export default class AvailableBaseLayers { private static LoadProviderIndex(): BaseLayer[] { // @ts-ignore X; // Import X to make sure the namespace is not optimized away - function l(id: string, name: string) { + function l(id: string, name: string) : BaseLayer{ try { - const layer: any = L.tileLayer.provider(id, undefined); - return { + const layer: any = () => L.tileLayer.provider(id, undefined); + const baseLayer : BaseLayer = { feature: null, id: id, name: name, @@ -161,6 +162,7 @@ export default class AvailableBaseLayers { min_zoom: layer.minzoom, max_zoom: layer.maxzoom } + return baseLayer } catch (e) { console.error("Could not find provided layer", name, e); return null; diff --git a/Logic/Actors/ImageSearcher.ts b/Logic/Actors/ImageSearcher.ts index 3ea510152a..165442d6f0 100644 --- a/Logic/Actors/ImageSearcher.ts +++ b/Logic/Actors/ImageSearcher.ts @@ -1,4 +1,4 @@ -import {ImagesInCategory, Wikidata, Wikimedia} from "../Web/Wikimedia"; +import {ImagesInCategory, Wikidata, Wikimedia} from "../ImageProviders/Wikimedia"; import {UIEventSource} from "../UIEventSource"; /** diff --git a/Logic/ImageProviders/AllImageProviders.ts b/Logic/ImageProviders/AllImageProviders.ts new file mode 100644 index 0000000000..89ae0a1427 --- /dev/null +++ b/Logic/ImageProviders/AllImageProviders.ts @@ -0,0 +1,9 @@ +import {Mapillary} from "./Mapillary"; +import {Wikimedia} from "./Wikimedia"; +import {Imgur} from "./Imgur"; + +export default class AllImageProviders{ + + public static ImageAttributionSource = [Imgur.singleton, Mapillary.singleton, Wikimedia.singleton] + +} \ No newline at end of file diff --git a/Logic/Web/ImageAttributionSource.ts b/Logic/ImageProviders/ImageAttributionSource.ts similarity index 91% rename from Logic/Web/ImageAttributionSource.ts rename to Logic/ImageProviders/ImageAttributionSource.ts index 689a32c46c..451909cfba 100644 --- a/Logic/Web/ImageAttributionSource.ts +++ b/Logic/ImageProviders/ImageAttributionSource.ts @@ -5,7 +5,6 @@ import BaseUIElement from "../../UI/BaseUIElement"; export default abstract class ImageAttributionSource { - private _cache = new Map>() GetAttributionFor(url: string): UIEventSource { @@ -22,6 +21,7 @@ export default abstract class ImageAttributionSource { public abstract SourceIcon(backlinkSource?: string) : BaseUIElement; protected abstract DownloadAttribution(url: string): UIEventSource; + /*Converts a value to a URL. Can return null if not applicable*/ public PrepareUrl(value: string): string{ return value; } diff --git a/Logic/Web/Imgur.ts b/Logic/ImageProviders/Imgur.ts similarity index 100% rename from Logic/Web/Imgur.ts rename to Logic/ImageProviders/Imgur.ts diff --git a/Logic/Web/ImgurUploader.ts b/Logic/ImageProviders/ImgurUploader.ts similarity index 100% rename from Logic/Web/ImgurUploader.ts rename to Logic/ImageProviders/ImgurUploader.ts diff --git a/Logic/Web/Mapillary.ts b/Logic/ImageProviders/Mapillary.ts similarity index 100% rename from Logic/Web/Mapillary.ts rename to Logic/ImageProviders/Mapillary.ts diff --git a/Logic/Web/Wikimedia.ts b/Logic/ImageProviders/Wikimedia.ts similarity index 82% rename from Logic/Web/Wikimedia.ts rename to Logic/ImageProviders/Wikimedia.ts index 4668f35116..9d74b77694 100644 --- a/Logic/Web/Wikimedia.ts +++ b/Logic/ImageProviders/Wikimedia.ts @@ -4,6 +4,7 @@ import BaseUIElement from "../../UI/BaseUIElement"; import Svg from "../../Svg"; import {UIEventSource} from "../UIEventSource"; import Link from "../../UI/Base/Link"; +import {Utils} from "../../Utils"; /** * This module provides endpoints for wikipedia/wikimedia and others @@ -138,21 +139,28 @@ export class Wikimedia extends ImageAttributionSource { "api.php?action=query&prop=imageinfo&iiprop=extmetadata&" + "titles=" + filename + "&format=json&origin=*"; - console.log("Getting attribution at ", url) - $.getJSON(url, function (data) { - const licenseInfo = new LicenseInfo(); - const license = data.query.pages[-1].imageinfo[0].extmetadata; + Utils.downloadJson(url).then( + data =>{ + const licenseInfo = new LicenseInfo(); + const license = (data.query.pages[-1].imageinfo ?? [])[0]?.extmetadata; + if(license === undefined){ + console.error("This file has no usable metedata or license attached... Please fix the license info file yourself!") + source.setData(null) + return; + } - licenseInfo.artist = license.Artist?.value; - licenseInfo.license = license.License?.value; - licenseInfo.copyrighted = license.Copyrighted?.value; - licenseInfo.attributionRequired = license.AttributionRequired?.value; - licenseInfo.usageTerms = license.UsageTerms?.value; - licenseInfo.licenseShortName = license.LicenseShortName?.value; - licenseInfo.credit = license.Credit?.value; - licenseInfo.description = license.ImageDescription?.value; - source.setData(licenseInfo); - }); + licenseInfo.artist = license.Artist?.value; + licenseInfo.license = license.License?.value; + licenseInfo.copyrighted = license.Copyrighted?.value; + licenseInfo.attributionRequired = license.AttributionRequired?.value; + licenseInfo.usageTerms = license.UsageTerms?.value; + licenseInfo.licenseShortName = license.LicenseShortName?.value; + licenseInfo.credit = license.Credit?.value; + licenseInfo.description = license.ImageDescription?.value; + source.setData(licenseInfo); + } + ) + return source; } diff --git a/Logic/Osm/OsmPreferences.ts b/Logic/Osm/OsmPreferences.ts index cd688b89c7..bff50eae60 100644 --- a/Logic/Osm/OsmPreferences.ts +++ b/Logic/Osm/OsmPreferences.ts @@ -97,7 +97,7 @@ export class OsmPreferences { public GetPreference(key: string, prefix: string = "mapcomplete-"): UIEventSource { key = prefix + key; - key = key.replace(/[:\\\/"' {}.%_]/g, '') + key = key.replace(/[:\\\/"' {}.%]/g, '') if (key.length >= 255) { throw "Preferences: key length to big"; } diff --git a/Models/BaseLayer.ts b/Models/BaseLayer.ts index 63c3abbc15..01eb8e9d75 100644 --- a/Models/BaseLayer.ts +++ b/Models/BaseLayer.ts @@ -3,7 +3,7 @@ import {TileLayer} from "leaflet"; export default interface BaseLayer { id: string, name: string, - layer: TileLayer, + layer: () => TileLayer, max_zoom: number, min_zoom: number; feature: any, diff --git a/Svg.ts b/Svg.ts index 3f0ff50fe8..9a5c94b8f2 100644 --- a/Svg.ts +++ b/Svg.ts @@ -104,11 +104,26 @@ export default class Svg { public static direction_svg() { return new Img(Svg.direction, true);} public static direction_ui() { return new FixedUiElement(Svg.direction_img);} - public static direction_gradient = " image/svg+xml " + public static direction_gradient = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient) public static direction_gradient_svg() { return new Img(Svg.direction_gradient, true);} public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);} + public static direction_masked = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_masked_img = Img.AsImageElement(Svg.direction_masked) + public static direction_masked_svg() { return new Img(Svg.direction_masked, true);} + public static direction_masked_ui() { return new FixedUiElement(Svg.direction_masked_img);} + + public static direction_outline = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_outline_img = Img.AsImageElement(Svg.direction_outline) + public static direction_outline_svg() { return new Img(Svg.direction_outline, true);} + public static direction_outline_ui() { return new FixedUiElement(Svg.direction_outline_img);} + + public static direction_stroke = " Created by potrace 1.15, written by Peter Selinger 2001-2017 image/svg+xml " + public static direction_stroke_img = Img.AsImageElement(Svg.direction_stroke) + public static direction_stroke_svg() { return new Img(Svg.direction_stroke, true);} + public static direction_stroke_ui() { return new FixedUiElement(Svg.direction_stroke_img);} + public static down = " image/svg+xml " public static down_img = Img.AsImageElement(Svg.down) public static down_svg() { return new Img(Svg.down, true);} @@ -319,4 +334,4 @@ export default class Svg { public static wikipedia_svg() { return new Img(Svg.wikipedia, true);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} +public static All = {"SocialImageForeground.svg": Svg.SocialImageForeground,"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"back.svg": Svg.back,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"circle.svg": Svg.circle,"clock.svg": Svg.clock,"close.svg": Svg.close,"compass.svg": Svg.compass,"cross_bottom_right.svg": Svg.cross_bottom_right,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"direction_masked.svg": Svg.direction_masked,"direction_outline.svg": Svg.direction_outline,"direction_stroke.svg": Svg.direction_stroke,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapcomplete_logo.svg": Svg.mapcomplete_logo,"mapillary.svg": Svg.mapillary,"mapillary_black.svg": Svg.mapillary_black,"min.svg": Svg.min,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-copyright.svg": Svg.osm_copyright,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"phone.svg": Svg.phone,"pin.svg": Svg.pin,"plus.svg": Svg.plus,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"ring.svg": Svg.ring,"search.svg": Svg.search,"send_email.svg": Svg.send_email,"share.svg": Svg.share,"square.svg": Svg.square,"star.svg": Svg.star,"star_half.svg": Svg.star_half,"star_outline.svg": Svg.star_outline,"star_outline_half.svg": Svg.star_outline_half,"statistics.svg": Svg.statistics,"translate.svg": Svg.translate,"up.svg": Svg.up,"wikidata.svg": Svg.wikidata,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/Base/Minimap.ts b/UI/Base/Minimap.ts new file mode 100644 index 0000000000..ad0f7eb7a7 --- /dev/null +++ b/UI/Base/Minimap.ts @@ -0,0 +1,144 @@ +import BaseUIElement from "../BaseUIElement"; +import * as L from "leaflet"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Loc from "../../Models/Loc"; +import BaseLayer from "../../Models/BaseLayer"; +import AvailableBaseLayers from "../../Logic/Actors/AvailableBaseLayers"; +import {Map} from "leaflet"; + +export default class Minimap extends BaseUIElement { + + private static _nextId = 0; + public readonly leafletMap: UIEventSource = new UIEventSource(undefined) + private readonly _id: string; + private readonly _background: UIEventSource; + private readonly _location: UIEventSource; + private _isInited = false; + private _allowMoving: boolean; + + constructor(options?: { + background?: UIEventSource, + location?: UIEventSource, + allowMoving?: boolean + } + ) { + super() + options = options ?? {} + this._background = options?.background ?? new UIEventSource(AvailableBaseLayers.osmCarto) + this._location = options?.location ?? new UIEventSource(undefined) + this._id = "minimap" + Minimap._nextId; + this._allowMoving = options.allowMoving ?? true; + Minimap._nextId++ + + } + + protected InnerConstructElement(): HTMLElement { + const div = document.createElement("div") + div.id = this._id; + div.style.height = "100%" + div.style.width = "100%" + div.style.minWidth = "40px" + div.style.minHeight = "40px" + const wrapper = document.createElement("div") + wrapper.appendChild(div) + const self = this; + // @ts-ignore + const resizeObserver = new ResizeObserver(_ => { + console.log("Change in size detected!") + self.InitMap(); + self.leafletMap?.data?.invalidateSize() + }); + + resizeObserver.observe(div); + return wrapper; + + } + + private InitMap() { + if (this._constructedHtmlElement === undefined) { + // This element isn't initialized yet + return; + } + + if (document.getElementById(this._id) === null) { + // not yet attached, we probably got some other event + return; + } + + if (this._isInited) { + return; + } + this._isInited = true; + const location = this._location; + + let currentLayer = this._background.data.layer() + const map = L.map(this._id, { + center: [location.data?.lat ?? 0, location.data?.lon ?? 0], + zoom: location.data?.zoom ?? 2, + layers: [currentLayer], + zoomControl: false, + attributionControl: false, + dragging: this._allowMoving, + scrollWheelZoom: this._allowMoving, + doubleClickZoom: this._allowMoving, + keyboard: this._allowMoving, + touchZoom: this._allowMoving + }); + + map.setMaxBounds( + [[-100, -200], [100, 200]] + ); + + this._background.addCallbackAndRun(layer => { + const newLayer = layer.layer() + if (currentLayer !== undefined) { + map.removeLayer(currentLayer); + } + currentLayer = newLayer; + map.addLayer(newLayer); + }) + + + let isRecursing = false; + map.on("moveend", function () { + if (isRecursing) { + return + } + if (map.getZoom() === location.data.zoom && + map.getCenter().lat === location.data.lat && + map.getCenter().lng === location.data.lon) { + return; + } + console.trace(map.getZoom(), map.getCenter(), location.data) + + location.data.zoom = map.getZoom(); + location.data.lat = map.getCenter().lat; + location.data.lon = map.getCenter().lng; + isRecursing = true; + location.ping(); + isRecursing = false; // This is ugly, I know + }) + + + location.addCallback(loc => { + const mapLoc = map.getCenter() + const dlat = Math.abs(loc.lat - mapLoc[0]) + const dlon = Math.abs(loc.lon - mapLoc[1]) + + if (dlat < 0.000001 && dlon < 0.000001 && map.getZoom() === loc.zoom) { + return; + } + map.setView([loc.lat, loc.lon], loc.zoom) + }) + + location.map(loc => loc.zoom) + .addCallback(zoom => { + if (Math.abs(map.getZoom() - zoom) > 0.1) { + map.setZoom(zoom, {}); + } + }) + + + this.leafletMap.setData(map) + } +} \ No newline at end of file diff --git a/UI/BaseUIElement.ts b/UI/BaseUIElement.ts index a919f0609c..2d046a3dd2 100644 --- a/UI/BaseUIElement.ts +++ b/UI/BaseUIElement.ts @@ -3,25 +3,21 @@ import {UIEventSource} from "../Logic/UIEventSource"; /** * A thin wrapper around a html element, which allows to generate a HTML-element. - * + * * Assumes a read-only configuration, so it has no 'ListenTo' */ export default abstract class BaseUIElement { + protected _constructedHtmlElement: HTMLElement; private clss: Set = new Set(); private style: string; private _onClick: () => void; private _onHover: UIEventSource; - - protected _constructedHtmlElement: HTMLElement; - - protected abstract InnerConstructElement(): HTMLElement; - public onClick(f: (() => void)) { this._onClick = f; this.SetClass("clickable") - if(this._constructedHtmlElement !== undefined){ + if (this._constructedHtmlElement !== undefined) { this._constructedHtmlElement.onclick = f; } return this; @@ -38,12 +34,13 @@ export default abstract class BaseUIElement { element.removeChild(element.firstChild); } const el = this.ConstructElement(); - if(el !== undefined){ + if (el !== undefined) { element.appendChild(el) } - + return this; } + /** * Adds all the relevant classes, space seperated */ @@ -55,7 +52,7 @@ export default abstract class BaseUIElement { if (this.clss.has(clss)) { continue; } - if(c === undefined || c === ""){ + if (c === undefined || c === "") { continue; } this.clss.add(c); @@ -74,19 +71,19 @@ export default abstract class BaseUIElement { } return this; } - - public HasClass(clss: string): boolean{ + + public HasClass(clss: string): boolean { return this.clss.has(clss) } public SetStyle(style: string): BaseUIElement { this.style = style; - if(this._constructedHtmlElement !== undefined){ + if (this._constructedHtmlElement !== undefined) { this._constructedHtmlElement.style.cssText = style; } return this; } - + /** * The same as 'Render', but creates a HTML element instead of the HTML representation */ @@ -99,68 +96,71 @@ export default abstract class BaseUIElement { return this._constructedHtmlElement } - if(this.InnerConstructElement === undefined){ - throw "ERROR! This is not a correct baseUIElement: "+this.constructor.name + if (this.InnerConstructElement === undefined) { + throw "ERROR! This is not a correct baseUIElement: " + this.constructor.name } -try{ - + try { - const el = this.InnerConstructElement(); - if(el === undefined){ - return undefined; - } + const el = this.InnerConstructElement(); - this._constructedHtmlElement = el; - const style = this.style - if (style !== undefined && style !== "") { - el.style.cssText = style - } - if (this.clss.size > 0) { - try{ - el.classList.add(...Array.from(this.clss)) - }catch(e){ - console.error("Invalid class name detected in:", Array.from(this.clss).join(" "),"\nErr msg is ",e) + if (el === undefined) { + return undefined; } - } - if (this._onClick !== undefined) { - const self = this; - el.onclick = (e) => { - // @ts-ignore - if (e.consumed) { - return; + this._constructedHtmlElement = el; + const style = this.style + if (style !== undefined && style !== "") { + el.style.cssText = style + } + if (this.clss.size > 0) { + try { + el.classList.add(...Array.from(this.clss)) + } catch (e) { + console.error("Invalid class name detected in:", Array.from(this.clss).join(" "), "\nErr msg is ", e) } - self._onClick(); - // @ts-ignore - e.consumed = true; } - el.style.pointerEvents = "all"; - el.style.cursor = "pointer"; - } - if (this._onHover !== undefined) { - const self = this; - el.addEventListener('mouseover', () => self._onHover.setData(true)); - el.addEventListener('mouseout', () => self._onHover.setData(false)); - } + if (this._onClick !== undefined) { + const self = this; + el.onclick = (e) => { + // @ts-ignore + if (e.consumed) { + return; + } + self._onClick(); + // @ts-ignore + e.consumed = true; + } + el.style.pointerEvents = "all"; + el.style.cursor = "pointer"; + } - if (this._onHover !== undefined) { - const self = this; - el.addEventListener('mouseover', () => self._onHover.setData(true)); - el.addEventListener('mouseout', () => self._onHover.setData(false)); - } + if (this._onHover !== undefined) { + const self = this; + el.addEventListener('mouseover', () => self._onHover.setData(true)); + el.addEventListener('mouseout', () => self._onHover.setData(false)); + } - return el}catch(e){ + if (this._onHover !== undefined) { + const self = this; + el.addEventListener('mouseover', () => self._onHover.setData(true)); + el.addEventListener('mouseout', () => self._onHover.setData(false)); + } + + return el + } catch (e) { const domExc = e as DOMException; - if(domExc){ - console.log("An exception occured", domExc.code, domExc.message, domExc.name ) + if (domExc) { + console.log("An exception occured", domExc.code, domExc.message, domExc.name) } console.error(e) -} + } } - - public AsMarkdown(): string{ - throw "AsMarkdown is not implemented by "+this.constructor.name + + public AsMarkdown(): string { + throw "AsMarkdown is not implemented by " + this.constructor.name } + + protected abstract InnerConstructElement(): HTMLElement; } \ No newline at end of file diff --git a/UI/BigComponents/Basemap.ts b/UI/BigComponents/Basemap.ts index 5c2844bd0f..4ad5bc8f6a 100644 --- a/UI/BigComponents/Basemap.ts +++ b/UI/BigComponents/Basemap.ts @@ -14,10 +14,14 @@ export class Basemap { currentLayer: UIEventSource, lastClickLocation?: UIEventSource<{ lat: number, lon: number }>, extraAttribution?: BaseUIElement) { + + console.log("Currentlayer is" ,currentLayer, currentLayer.data, currentLayer.data?.id) + let previousLayer = currentLayer.data.layer(); + this.map = L.map(leafletElementId, { center: [location.data.lat ?? 0, location.data.lon ?? 0], zoom: location.data.zoom ?? 2, - layers: [currentLayer.data.layer], + layers: [previousLayer], zoomControl: false, attributionControl: extraAttribution !== undefined }); @@ -42,16 +46,16 @@ export class Basemap { extraAttribution.AttachTo('leaflet-attribution') const self = this; - let previousLayer = currentLayer.data; currentLayer.addCallbackAndRun(layer => { - if (layer === previousLayer) { + const newLayer = layer.layer() + if (newLayer === previousLayer) { return; } if (previousLayer !== undefined) { - self.map.removeLayer(previousLayer.layer); + self.map.removeLayer(previousLayer); } - previousLayer = layer; - self.map.addLayer(layer.layer); + previousLayer = newLayer; + self.map.addLayer(newLayer); }) diff --git a/UI/BigComponents/MoreScreen.ts b/UI/BigComponents/MoreScreen.ts index 8976abb29a..bfab0567d6 100644 --- a/UI/BigComponents/MoreScreen.ts +++ b/UI/BigComponents/MoreScreen.ts @@ -29,7 +29,8 @@ export default class MoreScreen extends Combine { LanguagePicker.CreateLanguagePicker(Translations.t.index.title.SupportedLanguages()) .SetClass("absolute top-2 right-3"), new IndexText() - ]) + ]); + themeButtonStyle = "h-32 min-h-32 max-h-32 overflow-ellipsis overflow-hidden" themeListStyle = "md:grid md:grid-flow-row md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-g4 gap-4" } @@ -59,10 +60,23 @@ export default class MoreScreen extends Combine { private static createOfficialThemesList(state: State, buttonClass: string): BaseUIElement { let officialThemes = AllKnownLayouts.layoutsList - if (State.state.osmConnection.userDetails.data.csCount < Constants.userJourney.personalLayoutUnlock) { - officialThemes = officialThemes.filter(theme => theme.id !== personal.id) - } - let buttons = officialThemes.map((layout) => MoreScreen.createLinkButton(layout)?.SetClass(buttonClass)) + + let buttons = officialThemes.map((layout) => { + const button = MoreScreen.createLinkButton(layout)?.SetClass(buttonClass); + if(layout.id === personal.id){ + return new VariableUiElement( + State.state.osmConnection.userDetails.map(userdetails => userdetails.csCount) + .map(csCount => { + if(csCount < Constants.userJourney.personalLayoutUnlock){ + return undefined + }else{ + return button + } + }) + ) + } + return button; + }) let customGeneratorLink = MoreScreen.createCustomGeneratorButton(state) buttons.splice(0, 0, customGeneratorLink); diff --git a/UI/Image/AttributedImage.ts b/UI/Image/AttributedImage.ts index 260c5306f8..43bfc3ea21 100644 --- a/UI/Image/AttributedImage.ts +++ b/UI/Image/AttributedImage.ts @@ -1,7 +1,7 @@ import Combine from "../Base/Combine"; import Attribution from "./Attribution"; import Img from "../Base/Img"; -import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource"; +import ImageAttributionSource from "../../Logic/ImageProviders/ImageAttributionSource"; export class AttributedImage extends Combine { diff --git a/UI/Image/Attribution.ts b/UI/Image/Attribution.ts index 514bc475a9..0fddcc6f34 100644 --- a/UI/Image/Attribution.ts +++ b/UI/Image/Attribution.ts @@ -3,7 +3,7 @@ import Translations from "../i18n/Translations"; import BaseUIElement from "../BaseUIElement"; import {VariableUiElement} from "../Base/VariableUIElement"; import {UIEventSource} from "../../Logic/UIEventSource"; -import {LicenseInfo} from "../../Logic/Web/Wikimedia"; +import {LicenseInfo} from "../../Logic/ImageProviders/Wikimedia"; export default class Attribution extends VariableUiElement { diff --git a/UI/Image/ImageCarousel.ts b/UI/Image/ImageCarousel.ts index a28d89a2cf..7c5d90335b 100644 --- a/UI/Image/ImageCarousel.ts +++ b/UI/Image/ImageCarousel.ts @@ -6,10 +6,9 @@ import {AttributedImage} from "./AttributedImage"; import BaseUIElement from "../BaseUIElement"; import Img from "../Base/Img"; import Toggle from "../Input/Toggle"; -import ImageAttributionSource from "../../Logic/Web/ImageAttributionSource"; -import {Wikimedia} from "../../Logic/Web/Wikimedia"; -import {Mapillary} from "../../Logic/Web/Mapillary"; -import {Imgur} from "../../Logic/Web/Imgur"; +import {Wikimedia} from "../../Logic/ImageProviders/Wikimedia"; +import {Imgur} from "../../Logic/ImageProviders/Imgur"; +import {Mapillary} from "../../Logic/ImageProviders/Mapillary"; export class ImageCarousel extends Toggle { diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index cf5881f64e..58d9a37602 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -8,7 +8,7 @@ import BaseUIElement from "../BaseUIElement"; import LicensePicker from "../BigComponents/LicensePicker"; import Toggle from "../Input/Toggle"; import FileSelectorButton from "../Input/FileSelectorButton"; -import ImgurUploader from "../../Logic/Web/ImgurUploader"; +import ImgurUploader from "../../Logic/ImageProviders/ImgurUploader"; import UploadFlowStateUI from "../BigComponents/UploadFlowStateUI"; import LayerConfig from "../../Customizations/JSON/LayerConfig"; diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts index 4be40b99e3..c356d34a85 100644 --- a/UI/Input/DirectionInput.ts +++ b/UI/Input/DirectionInput.ts @@ -2,21 +2,30 @@ import {InputElement} from "./InputElement"; import {UIEventSource} from "../../Logic/UIEventSource"; import Combine from "../Base/Combine"; import Svg from "../../Svg"; +import BaseUIElement from "../BaseUIElement"; import {FixedUiElement} from "../Base/FixedUiElement"; +import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; /** * Selects a direction in degrees */ export default class DirectionInput extends InputElement { + public static constructMinimap: ((any) => BaseUIElement); + private readonly _location: UIEventSource; public readonly IsSelected: UIEventSource = new UIEventSource(false); private readonly value: UIEventSource; + private background; - constructor(value?: UIEventSource) { + constructor(mapBackground: UIEventSource, + location: UIEventSource, + value?: UIEventSource) { super(); + this._location = location; this.value = value ?? new UIEventSource(undefined); - + this.background = mapBackground; } GetValue(): UIEventSource { @@ -30,16 +39,23 @@ export default class DirectionInput extends InputElement { protected InnerConstructElement(): HTMLElement { + let map: BaseUIElement = new FixedUiElement("") + if (!Utils.runningFromConsole) { + map = DirectionInput.constructMinimap({ + background: this.background, + allowMoving: false, + location: this._location + }) + } const element = new Combine([ - new FixedUiElement("").SetClass("w-full h-full absolute top-0 left-O rounded-full"), - Svg.direction_svg().SetStyle( + Svg.direction_stroke_svg().SetStyle( `position: absolute;top: 0;left: 0;width: 100%;height: 100%;transform:rotate(${this.value.data ?? 0}deg);`) - .SetClass("direction-svg"), - Svg.compass_svg().SetStyle( - "position: absolute;top: 0;left: 0;width: 100%;height: 100%;") + .SetClass("direction-svg relative") + .SetStyle("z-index: 1000"), + map.SetClass("w-full h-full absolute top-0 left-O rounded-full overflow-hidden"), ]) - .SetStyle("position:relative;display:block;width: min(100%, 25em); padding-top: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .SetStyle("position:relative;display:block;width: min(100%, 25em); height: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") .ConstructElement() diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 0471e660eb..475f6cf423 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -1,7 +1,6 @@ import {DropDown} from "./DropDown"; import * as EmailValidator from "email-validator"; import {parsePhoneNumberFromString} from "libphonenumber-js"; -import InputElementMap from "./InputElementMap"; import {InputElement} from "./InputElement"; import {TextField} from "./TextField"; import {UIElement} from "../UIElement"; @@ -12,6 +11,7 @@ import OpeningHoursInput from "../OpeningHours/OpeningHoursInput"; import DirectionInput from "./DirectionInput"; import ColorPicker from "./ColorPicker"; import {Utils} from "../../Utils"; +import Loc from "../../Models/Loc"; interface TextFieldDef { name: string, @@ -19,7 +19,8 @@ interface TextFieldDef { isValid: ((s: string, country?: () => string) => boolean), reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { - location: [number, number] + location: [number, number], + mapBackgroundLayer?: UIEventSource }) => InputElement, inputmode?: string @@ -118,8 +119,12 @@ export default class ValidatedTextField { str = "" + str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 }, str => str, - (value) => { - return new DirectionInput(value); + (value, options) => { + return new DirectionInput(options.mapBackgroundLayer , new UIEventSource({ + lat: options.location[0], + lon: options.location[1], + zoom: 19 + }),value); }, "numeric" ), @@ -216,16 +221,6 @@ export default class ValidatedTextField { * {string (typename) --> TextFieldDef} */ public static AllTypes = ValidatedTextField.allTypesDict(); - - public static TypeDropdown(): DropDown { - const values: { value: string, shown: string }[] = []; - const expl = ValidatedTextField.tpList; - for (const key in expl) { - values.push({value: expl[key].name, shown: `${expl[key].name} - ${expl[key].explanation}`}) - } - return new DropDown("", values) - } - public static InputForType(type: string, options?: { placeholder?: string | UIElement, value?: UIEventSource, @@ -235,7 +230,8 @@ export default class ValidatedTextField { textAreaRows?: number, isValid?: ((s: string, country: () => string) => boolean), country?: () => string, - location?: [number /*lat*/, number /*lon*/] + location?: [number /*lat*/, number /*lon*/], + mapBackgroundLayer?: UIEventSource }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -269,90 +265,16 @@ export default class ValidatedTextField { if (tp.inputHelper) { input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(), { - location: options.location + location: options.location, + mapBackgroundLayer: options.mapBackgroundLayer + }), - (a, b) => a, // We can ignore b, as they are linked earlier + (a, _) => a, // We can ignore b, as they are linked earlier a => [a, a] ); } return input; } - - public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined): InputElement { - const isValid = ValidatedTextField.AllTypes[type].isValid; - extraValidation = extraValidation ?? (() => true) - - const fromString = str => { - if (!isValid(str)) { - return undefined; - } - const n = Number(str); - if (!extraValidation(n)) { - return undefined; - } - return n; - }; - const toString = num => { - if (num === undefined) { - return undefined; - } - return "" + num; - }; - const textField = ValidatedTextField.InputForType(type); - return new InputElementMap(textField, (n0, n1) => n0 === n1, fromString, toString) - } - - public static KeyInput(allowEmpty: boolean = false): InputElement { - - function fromString(str) { - if (str?.match(/^[a-zA-Z][a-zA-Z0-9:_-]*$/)) { - return str; - } - if (str === "" && allowEmpty) { - return ""; - } - - return undefined - } - - const toString = str => str - - function isSame(str0, str1) { - return str0 === str1; - } - - const textfield = new TextField({ - placeholder: "key", - isValid: str => fromString(str) !== undefined, - value: new UIEventSource("") - }); - - return new InputElementMap(textfield, isSame, fromString, toString); - } - - static Mapped(fromString: (str) => T, toString: (T) => string, options?: { - placeholder?: string | UIElement, - type?: string, - value?: UIEventSource, - startValidated?: boolean, - textArea?: boolean, - textAreaRows?: number, - isValid?: ((string: string) => boolean), - country?: () => string - }): InputElement { - let textField: InputElement; - if (options?.type) { - textField = ValidatedTextField.InputForType(options.type, options); - } else { - textField = new TextField(options); - } - return new InputElementMap( - textField, (a, b) => a === b, - fromString, toString - ); - - } - public static HelpText(): string { const explanations = ValidatedTextField.tpList.map(type => ["## " + type.name, "", type.explanation].join("\n")).join("\n\n") return "# Available types for text fields\n\nThe listed types here trigger a special input element. Use them in `tagrendering.freeform.type` of your tagrendering to activate them\n\n" + explanations @@ -363,7 +285,8 @@ export default class ValidatedTextField { isValid?: ((s: string, country?: () => string) => boolean), reformat?: ((s: string, country?: () => string) => string), inputHelper?: (value: UIEventSource, options?: { - location: [number, number] + location: [number, number], + mapBackgroundLayer: UIEventSource }) => InputElement, inputmode?: string): TextFieldDef { diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 91a74a1f9a..21cac5c976 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -331,7 +331,8 @@ export default class TagRenderingQuestion extends UIElement { let input: InputElement = ValidatedTextField.InputForType(this._configuration.freeform.type, { isValid: (str) => (str.length <= 255), country: () => this._tags.data._country, - location: [this._tags.data._lat, this._tags.data._lon] + location: [this._tags.data._lat, this._tags.data._lon], + mapBackgroundLayer: State.state.backgroundLayer }); if (this._applicableUnit) { diff --git a/UI/ShowDataLayer.ts b/UI/ShowDataLayer.ts index 77e5eff9b8..cb48a10fb7 100644 --- a/UI/ShowDataLayer.ts +++ b/UI/ShowDataLayer.ts @@ -15,13 +15,18 @@ export default class ShowDataLayer { private _layerDict; private readonly _leafletMap: UIEventSource; private _cleanCount = 0; + private readonly _enablePopups: boolean; + private readonly _features : UIEventSource<{ feature: any, freshness: Date }[]> constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>, leafletMap: UIEventSource, - layoutToUse: UIEventSource) { + layoutToUse: UIEventSource, + enablePopups= true, + zoomToFeatures = false) { this._leafletMap = leafletMap; + this._enablePopups = enablePopups; + this._features = features; const self = this; - const mp = leafletMap.data; self._layerDict = {}; layoutToUse.addCallbackAndRun(layoutToUse => { @@ -39,7 +44,9 @@ export default class ShowDataLayer { if (features.data === undefined) { return; } - if (leafletMap.data === undefined) { + const mp = leafletMap.data; + + if(mp === undefined){ return; } @@ -68,6 +75,11 @@ export default class ShowDataLayer { mp.addLayer(geoLayer) } + if(zoomToFeatures){ + mp.fitBounds(geoLayer.getBounds()) + } + + State.state.selectedElement.ping(); } @@ -77,6 +89,7 @@ export default class ShowDataLayer { } + private createStyleFor(feature) { const tagsSource = State.state.allElements.addOrGetElement(feature); // Every object is tied to exactly one layer @@ -97,9 +110,13 @@ export default class ShowDataLayer { } const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0)); + const baseElement = style.icon.html; + if(!this._enablePopups){ + baseElement.SetStyle("cursor: initial !important") + } return L.marker(latLng, { icon: L.divIcon({ - html: style.icon.html.ConstructElement(), + html: baseElement.ConstructElement(), className: style.icon.className, iconAnchor: style.icon.iconAnchor, iconUrl: style.icon.iconUrl, @@ -115,10 +132,12 @@ export default class ShowDataLayer { console.warn("No layer found for object (probably a now disabled layer)", feature, this._layerDict) return; } - if (layer.title === undefined) { + 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, @@ -171,15 +190,15 @@ export default class ShowDataLayer { } private CreateGeojsonLayer(): L.Layer { - const self = this; - const data = { - type: "FeatureCollection", - features: [] - } - // @ts-ignore - return L.geoJSON(data, { - style: feature => self.createStyleFor(feature), - pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), + const self = this; + const data = { + type: "FeatureCollection", + features: [] + } + // @ts-ignore + return L.geoJSON(data, { + style: feature => self.createStyleFor(feature), + pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng), onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer) }); diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index b1f34f7576..edc6f730bf 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -21,6 +21,10 @@ import LayerConfig from "../Customizations/JSON/LayerConfig"; import Title from "./Base/Title"; import Table from "./Base/Table"; import Histogram from "./BigComponents/Histogram"; +import Loc from "../Models/Loc"; +import ShowDataLayer from "./ShowDataLayer"; +import Minimap from "./Base/Minimap"; +import {Utils} from "../Utils"; export default class SpecialVisualizations { @@ -32,7 +36,6 @@ export default class SpecialVisualizations { example?: string, args: { name: string, defaultValue?: string, doc: string }[] }[] = - [ { funcName: "all_tags", @@ -86,7 +89,80 @@ export default class SpecialVisualizations { return new ImageUploadFlow(tags, args[0]) } }, + { + funcName: "minimap", + docs: "A small map showing the selected feature. Note that no styling is applied, wrap this in a div", + args: [ + { + doc: "The zoomlevel: the higher, the more zoomed in with 1 being the entire world and 19 being really close", + name: "zoomlevel", + defaultValue: "18" + }, + { + doc: "(Matches all resting arguments) This argument should be the key of a property of the feature. The corresponding value is interpreted as either the id or the a list of ID's. The features with these ID's will be shown on this minimap.", + name: "idKey", + defaultValue: "id" + } + ], + example: "`{minimap()}`, `{minimap(17, id, _list_of_embedded_feature_ids_calculated_by_calculated_tag):height:10rem; border: 2px solid black}`", + constr: (state, tagSource, args) => { + const keys = [...args] + keys.splice(0, 1) + const featureStore = state.allElements.ContainingFeatures + const featuresToShow: UIEventSource<{ freshness: Date, feature: any }[]> = tagSource.map(properties => { + const values: string[] = Utils.NoNull(keys.map(key => properties[key])) + const features: { freshness: Date, feature: any }[] = [] + for (const value of values) { + let idList = [value] + if (value.startsWith("[")) { + // This is a list of values + idList = JSON.parse(value) + } + for (const id of idList) { + features.push({ + freshness: new Date(), + feature: featureStore.get(id) + }) + } + } + return features + }) + const properties = tagSource.data; + + let zoom = 18 + if (args[0]) { + const parsed = Number(args[0]) + if (!isNaN(parsed) && parsed > 0 && parsed < 25) { + zoom = parsed; + } + } + const minimap = new Minimap( + { + background: state.backgroundLayer, + location: new UIEventSource({ + lat: Number(properties._lat), + lon: Number(properties._lon), + zoom: zoom + }), + allowMoving: false + } + ) + + new ShowDataLayer( + featuresToShow, + minimap.leafletMap, + State.state.layoutToUse, + false, + true + ) + + + minimap.SetStyle("overflow: hidden; pointer-events: none;") + return minimap; + + } + }, { funcName: "reviews", docs: "Adds an overview of the mangrove-reviews of this object. Mangrove.Reviews needs - in order to identify the reviewed object - a coordinate and a name. By default, the name of the object is given, but this can be overwritten", @@ -169,7 +245,7 @@ export default class SpecialVisualizations { defaultValue: "" }, { - name: "colors", + name: "colors*", doc: "(Matches all resting arguments - optional) Matches a regex onto a color value, e.g. `3[a-zA-Z+-]*:#33cc33`" } @@ -260,33 +336,33 @@ export default class SpecialVisualizations { } }, - {funcName: "canonical", - docs: "Converts a short, canonical value into the long, translated text", - example: "{canonical(length)} will give 42 metre (in french)", - args:[{ - name:"key", - doc: "The key of the tag to give the canonical text for" - }], - constr: (state, tagSource, args) => { - const key = args [0] - return new VariableUiElement( - tagSource.map(tags => tags[key]).map(value => { - if(value === undefined){ - return undefined - } - const unit = state.layoutToUse.data.units.filter(unit => unit.isApplicableToKey(key))[0] - if(unit === undefined){ - return value; - } - - return unit.asHumanLongValue(value); - - }, - [ state.layoutToUse]) - - - ) - }} + { + funcName: "canonical", + docs: "Converts a short, canonical value into the long, translated text", + example: "{canonical(length)} will give 42 metre (in french)", + args: [{ + name: "key", + doc: "The key of the tag to give the canonical text for" + }], + constr: (state, tagSource, args) => { + const key = args [0] + return new VariableUiElement( + tagSource.map(tags => tags[key]).map(value => { + if (value === undefined) { + return undefined + } + const unit = state.layoutToUse.data.units.filter(unit => unit.isApplicableToKey(key))[0] + if (unit === undefined) { + return value; + } + + return unit.asHumanLongValue(value); + + }, + [state.layoutToUse]) + ) + } + } ] static HelpMessage: BaseUIElement = SpecialVisualizations.GenHelpMessage(); @@ -313,7 +389,7 @@ export default class SpecialVisualizations { return new Combine([ new Title("Special tag renderings", 3), "In a tagrendering, some special values are substituted by an advanced UI-element. This allows advanced features and visualizations to be reused by custom themes or even to query third-party API's.", - "General usage is {func_name()} or {func_name(arg, someotherarg)}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args", + "General usage is {func_name()}, {func_name(arg, someotherarg)} or {func_name(args):cssStyle}. Note that you do not need to use quotes around your arguments, the comma is enough to seperate them. This also implies you cannot use a comma in your args", ...helpTexts ] ); diff --git a/UI/SubstitutedTranslation.ts b/UI/SubstitutedTranslation.ts index c99699dda4..5bd7c68829 100644 --- a/UI/SubstitutedTranslation.ts +++ b/UI/SubstitutedTranslation.ts @@ -17,14 +17,14 @@ export class SubstitutedTranslation extends VariableUiElement { super( tagsSource.map(tags => { const txt = Utils.SubstituteKeys(translation.txt, tags) - if (txt === undefined) { + if (txt === undefined) { return undefined } - return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tagsSource)) + return new Combine(SubstitutedTranslation.EvaluateSpecialComponents(txt, tagsSource)) }, [Locale.language]) ) - - + + this.SetClass("w-full") } @@ -34,13 +34,14 @@ export class SubstitutedTranslation extends VariableUiElement { for (const knownSpecial of SpecialVisualizations.specialVisualizations) { // 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}\\((.*?)\\)(:.*)?}(.*)`); if (matched != null) { // We found a special component that should be brought to live const partBefore = SubstitutedTranslation.EvaluateSpecialComponents(matched[1], tags); const argument = matched[2].trim(); - const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[3], tags); + const style = matched[3]?.substring(1) ?? "" + const partAfter = SubstitutedTranslation.EvaluateSpecialComponents(matched[4], tags); try { const args = knownSpecial.args.map(arg => arg.defaultValue ?? ""); if (argument.length > 0) { @@ -56,13 +57,14 @@ export class SubstitutedTranslation extends VariableUiElement { let element: BaseUIElement = new FixedUiElement(`Constructing ${knownSpecial}(${args.join(", ")})`) - try{ - element = knownSpecial.constr(State.state, tags, args); - }catch(e){ + try { + element = knownSpecial.constr(State.state, tags, args); + element.SetStyle(style) + } catch (e) { console.error("SPECIALRENDERING FAILED for", tags.data.id, e) element = new FixedUiElement(`Could not generate special rendering for ${knownSpecial}(${args.join(", ")}) ${e}`).SetClass("alert") } - + return [...partBefore, element, ...partAfter] } catch (e) { console.error(e); diff --git a/Utils.ts b/Utils.ts index 111939b1ad..fa3e55ef88 100644 --- a/Utils.ts +++ b/Utils.ts @@ -1,5 +1,4 @@ import * as colors from "./assets/colors.json" -import {Util} from "leaflet"; export class Utils { @@ -324,6 +323,34 @@ export class Utils { } return result; } + + public static externalDownloadFunction: (url: string) => Promise; + + public static downloadJson(url: string): Promise{ + if(this.externalDownloadFunction !== undefined){ + return this.externalDownloadFunction(url) + } + + return new Promise( + (resolve, reject) => { + try{ + const xhr = new XMLHttpRequest(); + xhr.onload = () => { + if (xhr.status == 200) { + resolve(JSON.parse(xhr.response)) + } else { + reject(xhr.statusText) + } + }; + xhr.open('GET', url); + xhr.send(); + }catch(e){ + reject(e) + } + } + ) + + } /** * Triggers a 'download file' popup which will download the contents diff --git a/assets/contributors.json b/assets/contributors.json index 02c30c2ced..2b2edda4ff 100644 --- a/assets/contributors.json +++ b/assets/contributors.json @@ -1 +1 @@ -{"contributors":[{"contributor":"Pieter Vander Vennet", "commits":738},{"contributor":"pietervdvn", "commits":718},{"contributor":"Weblate", "commits":35},{"contributor":"Tobias", "commits":35},{"contributor":"Christian Neumann", "commits":33},{"contributor":"Win Olario", "commits":31},{"contributor":"Pieter Fiers", "commits":31},{"contributor":"Sebastian Kürten", "commits":16},{"contributor":"Marco", "commits":16},{"contributor":"Joost", "commits":16},{"contributor":"ToastHawaii", "commits":15},{"contributor":"J. Lavoie", "commits":14},{"contributor":"Bavo Vanderghote", "commits":12},{"contributor":"Artem", "commits":12},{"contributor":"Supaplex", "commits":9},{"contributor":"Jacque Fresco", "commits":9},{"contributor":"Midgard", "commits":8},{"contributor":"Mateusz Konieczny", "commits":8},{"contributor":"yopaseopor", "commits":7},{"contributor":"Flo Edelmann", "commits":7},{"contributor":"Binnette", "commits":7},{"contributor":"Allan Nordhøy", "commits":7},{"contributor":"pelderson", "commits":6},{"contributor":"lvgx", "commits":6},{"contributor":"dependabot[bot]", "commits":6},{"contributor":"Alexey Shabanov", "commits":6},{"contributor":"SiegbjornSitumeang", "commits":4},{"contributor":"Polgár Sándor", "commits":4},{"contributor":"Hiroshi Miura", "commits":4},{"contributor":"vankos", "commits":3},{"contributor":"Léo Villeveygoux", "commits":3},{"contributor":"JCGF-OSM", "commits":3},{"contributor":"Jan Zabel", "commits":3},{"contributor":"Hosted Weblate", "commits":3},{"contributor":"David Haberthür", "commits":3},{"contributor":"快乐的老鼠宝宝", "commits":2},{"contributor":"Wiktor Przybylski", "commits":2},{"contributor":"Vinicius", "commits":2},{"contributor":"Stanislas Gueniffey", "commits":2},{"contributor":"Robin van der Linde", "commits":2},{"contributor":"riiga", "commits":2},{"contributor":"pbarban", "commits":2},{"contributor":"mic140", "commits":2},{"contributor":"Leo Alcaraz", "commits":2},{"contributor":"Jose Luis Infante", "commits":2},{"contributor":"Heiko", "commits":2},{"contributor":"graveelius", "commits":2},{"contributor":"Tomas Fiers", "commits":1},{"contributor":"Thibault Molleman", "commits":1},{"contributor":"tbowdecl97", "commits":1},{"contributor":"Sebastian", "commits":1},{"contributor":"Sean Young", "commits":1},{"contributor":"Schouppe Joost", "commits":1},{"contributor":"Noémie", "commits":1},{"contributor":"mozita", "commits":1},{"contributor":"Michał Targoński", "commits":1},{"contributor":"Iváns", "commits":1},{"contributor":"Eric Armijo", "commits":1},{"contributor":"Damian Pułka", "commits":1},{"contributor":"Carlos Ramos Carreño", "commits":1},{"contributor":"Beardhatcode", "commits":1}]} \ No newline at end of file +{"contributors":[{"contributor":"pietervdvn", "commits":794},{"contributor":"Pieter Vander Vennet", "commits":744},{"contributor":"Weblate", "commits":38},{"contributor":"Tobias", "commits":35},{"contributor":"Christian Neumann", "commits":33},{"contributor":"Win Olario", "commits":31},{"contributor":"Pieter Fiers", "commits":31},{"contributor":"Sebastian Kürten", "commits":17},{"contributor":"Joost", "commits":17},{"contributor":"Marco", "commits":16},{"contributor":"Artem", "commits":16},{"contributor":"Allan Nordhøy", "commits":16},{"contributor":"ToastHawaii", "commits":15},{"contributor":"Supaplex", "commits":14},{"contributor":"J. Lavoie", "commits":14},{"contributor":"Bavo Vanderghote", "commits":12},{"contributor":"Jacque Fresco", "commits":9},{"contributor":"Midgard", "commits":8},{"contributor":"Mateusz Konieczny", "commits":8},{"contributor":"yopaseopor", "commits":7},{"contributor":"Hosted Weblate", "commits":7},{"contributor":"Flo Edelmann", "commits":7},{"contributor":"Binnette", "commits":7},{"contributor":"pelderson", "commits":6},{"contributor":"lvgx", "commits":6},{"contributor":"dependabot[bot]", "commits":6},{"contributor":"Alexey Shabanov", "commits":6},{"contributor":"SiegbjornSitumeang", "commits":4},{"contributor":"Polgár Sándor", "commits":4},{"contributor":"Hiroshi Miura", "commits":4},{"contributor":"Wiktor Przybylski", "commits":3},{"contributor":"vankos", "commits":3},{"contributor":"Léo Villeveygoux", "commits":3},{"contributor":"JCGF-OSM", "commits":3},{"contributor":"Jan Zabel", "commits":3},{"contributor":"Erik Palm", "commits":3},{"contributor":"David Haberthür", "commits":3},{"contributor":"快乐的老鼠宝宝", "commits":2},{"contributor":"Vinicius", "commits":2},{"contributor":"Stanislas Gueniffey", "commits":2},{"contributor":"Robin van der Linde", "commits":2},{"contributor":"riiga", "commits":2},{"contributor":"pbarban", "commits":2},{"contributor":"mic140", "commits":2},{"contributor":"Leo Alcaraz", "commits":2},{"contributor":"Jose Luis Infante", "commits":2},{"contributor":"Heiko", "commits":2},{"contributor":"graveelius", "commits":2},{"contributor":"Damian Tokarski", "commits":2},{"contributor":"Tomas Fiers", "commits":1},{"contributor":"Thibault Molleman", "commits":1},{"contributor":"tbowdecl97", "commits":1},{"contributor":"Sebastian", "commits":1},{"contributor":"Sean Young", "commits":1},{"contributor":"Schouppe Joost", "commits":1},{"contributor":"Noémie", "commits":1},{"contributor":"mozita", "commits":1},{"contributor":"Michał Targoński", "commits":1},{"contributor":"liimee", "commits":1},{"contributor":"Jeff Huang", "commits":1},{"contributor":"Iváns", "commits":1},{"contributor":"Eric Armijo", "commits":1},{"contributor":"Damian Pułka", "commits":1},{"contributor":"Carlos Ramos Carreño", "commits":1},{"contributor":"Beardhatcode", "commits":1}]} \ No newline at end of file diff --git a/assets/layers/public_bookcase/public_bookcase.json b/assets/layers/public_bookcase/public_bookcase.json index f3db0d0b7b..6d8ecc87e5 100644 --- a/assets/layers/public_bookcase/public_bookcase.json +++ b/assets/layers/public_bookcase/public_bookcase.json @@ -72,6 +72,9 @@ ], "tagRenderings": [ "images", + { + "render": "{minimap():height: 9rem; border-radius: 2.5rem; overflow:hidden;border:1px solid gray}" + }, { "render": { "en": "The name of this bookcase is {name}", diff --git a/assets/svg/direction_gradient.svg b/assets/svg/direction_gradient.svg index 54af33ba00..50b10be0ee 100644 --- a/assets/svg/direction_gradient.svg +++ b/assets/svg/direction_gradient.svg @@ -6,48 +6,97 @@ xmlns:svg="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" - id="svg8" - version="1.1" - viewBox="0 0 100 100" - height="100" - width="100"> - - - - image/svg+xml - - - - + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + version="1.0" + width="860.50732pt" + height="860.50732pt" + viewBox="0 0 860.50732 860.50732" + preserveAspectRatio="xMidYMid meet" + id="svg14" + sodipodi:docname="direction_gradient.svg" + inkscape:version="0.92.5 (2060ec1f9f, 2020-04-08)"> + id="defs18"> + inkscape:collect="always" + id="linearGradient832"> + id="stop828" /> + id="stop830" /> + + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + style="fill:url(#radialGradient838);fill-opacity:1;stroke:none;stroke-width:2.83464575;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 735.79979,124.70799 C 654.79116,43.598883 544.88842,-2.0206645 430.25389,-2.121103 315.61937,-2.0206592 205.71663,43.598888 124.70801,124.70799 l 305.54588,305.54589 z" + id="path836" + inkscape:connector-curvature="0" + sodipodi:nodetypes="ccccc" /> diff --git a/assets/svg/direction_masked.svg b/assets/svg/direction_masked.svg new file mode 100644 index 0000000000..8a591c2138 --- /dev/null +++ b/assets/svg/direction_masked.svg @@ -0,0 +1,70 @@ + + + + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + diff --git a/assets/svg/direction_outline.svg b/assets/svg/direction_outline.svg new file mode 100644 index 0000000000..679c50f380 --- /dev/null +++ b/assets/svg/direction_outline.svg @@ -0,0 +1,72 @@ + + + + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + diff --git a/assets/svg/direction_stroke.svg b/assets/svg/direction_stroke.svg new file mode 100644 index 0000000000..af96495107 --- /dev/null +++ b/assets/svg/direction_stroke.svg @@ -0,0 +1,72 @@ + + + + + + + + +Created by potrace 1.15, written by Peter Selinger 2001-2017 + + + image/svg+xml + + + + + + + diff --git a/assets/tagRenderings/questions.json b/assets/tagRenderings/questions.json index 0ac2cd7f77..16eb121e3c 100644 --- a/assets/tagRenderings/questions.json +++ b/assets/tagRenderings/questions.json @@ -5,6 +5,9 @@ "reviews": { "render": "{reviews()}" }, + "minimap": { + "render": "{minimap(19, id): width:100%; height:6rem; border-radius:999rem; overflow: hidden; pointer-events: none;}" + }, "phone": { "question": { "en": "What is the phone number of {name}?", diff --git a/assets/themes/campersites/campersites.json b/assets/themes/campersites/campersites.json index 512ed671d6..eca693f1a1 100644 --- a/assets/themes/campersites/campersites.json +++ b/assets/themes/campersites/campersites.json @@ -15,7 +15,8 @@ "ru": "Найти места остановки, чтобы провести ночь в автофургоне", "ja": "キャンパーと夜を共にするキャンプサイトを見つける", "fr": "Trouver des sites pour passer la nuit avec votre camping-car", - "zh_Hant": "露營者尋找渡過夜晚的場地" + "zh_Hant": "露營者尋找渡過夜晚的場地", + "nl": "Vind locaties waar je de nacht kan doorbrengen met je mobilehome" }, "description": { "en": "This site collects all official camper stopover places and places where you can dump grey and black water. You can add details about the services provided and the cost. Add pictures and reviews. This is a website and a webapp. The data is stored in OpenStreetMap, so it will be free forever and can be re-used by any app.", diff --git a/assets/themes/climbing/climbing.json b/assets/themes/climbing/climbing.json index 17b0925d83..a999399141 100644 --- a/assets/themes/climbing/climbing.json +++ b/assets/themes/climbing/climbing.json @@ -781,7 +781,8 @@ "en": "Climbing is not possible here", "de": "Hier kann nicht geklettert werden", "ja": "ここでは登ることができない", - "nb_NO": "Klatring er ikke mulig her" + "nb_NO": "Klatring er ikke mulig her", + "nl": "Klimmen is hier niet mogelijk" }, "hideInAnswer": true }, @@ -795,7 +796,8 @@ "en": "Climbing is possible here", "de": "Hier kann geklettert werden", "ja": "ここでは登ることができる", - "nb_NO": "Klatring er mulig her" + "nb_NO": "Klatring er mulig her", + "nl": "Klimmen is hier niet toegelaten" } }, { @@ -804,7 +806,8 @@ "en": "Climbing is not possible here", "de": "Hier kann nicht geklettert werden", "ja": "ここでは登ることができない", - "nb_NO": "Klatring er ikke mulig her" + "nb_NO": "Klatring er ikke mulig her", + "nl": "Klimmen is hier niet toegelaten" } } ] @@ -848,7 +851,8 @@ "question": { "en": "Is there a (unofficial) website with more informations (e.g. topos)?", "de": "Gibt es eine (inoffizielle) Website mit mehr Informationen (z.B. Topos)?", - "ja": "もっと情報のある(非公式の)ウェブサイトはありますか(例えば、topos)?" + "ja": "もっと情報のある(非公式の)ウェブサイトはありますか(例えば、topos)?", + "nl": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?" }, "condition": { "and": [ @@ -870,13 +874,15 @@ { "if": "_embedding_feature:access=yes", "then": { - "en": "The containing feature states that this is publicly accessible
{_embedding_feature:access:description}" + "en": "The containing feature states that this is publicly accessible
{_embedding_feature:access:description}", + "nl": "Een omvattend element geeft aan dat dit publiek toegangkelijk is
{_embedding_feature:access:description}" } }, { "if": "_embedding_feature:access=permit", "then": { - "en": "The containing feature states that a permit is needed to access
{_embedding_feature:access:description}" + "en": "The containing feature states that a permit is needed to access
{_embedding_feature:access:description}", + "nl": "Een omvattend element geeft aan dat een toelating nodig is om hier te klimmen
{_embedding_feature:access:description}" } }, { diff --git a/index.ts b/index.ts index 472fb943fb..83abe95d48 100644 --- a/index.ts +++ b/index.ts @@ -14,10 +14,12 @@ import Translations from "./UI/i18n/Translations"; import CountryCoder from "latlon2country" import SimpleMetaTagger from "./Logic/SimpleMetaTagger"; +import Minimap from "./UI/Base/Minimap"; +import DirectionInput from "./UI/Input/DirectionInput"; -// Workaround for a stupid crash: inject the function +// Workaround for a stupid crash: inject some functions which would give stupid circular dependencies or crash the other nodejs scripts SimpleMetaTagger.coder = new CountryCoder("https://pietervdvn.github.io/latlon2country/"); - +DirectionInput.constructMinimap = options => new Minimap(options) let defaultLayout = "" // --------------------- Special actions based on the parameters ----------------- diff --git a/langs/en.json b/langs/en.json index 23633c0c04..dda19d6008 100644 --- a/langs/en.json +++ b/langs/en.json @@ -72,6 +72,12 @@ "emailOf": "What is the email address of {category}?", "emailIs": "The email address of this {category} is {email}" }, + "morescreen": { + "intro": "

More thematic maps?

Do you enjoy collecting geodata?
There are more themes available.", + "requestATheme": "If you want a custom-built quest, request it in the issue tracker", + "streetcomplete": "Another, similar application is StreetComplete.", + "createYourOwnTheme": "Create your own MapComplete theme from scratch" + }, "sharescreen": { "intro": "

Share this map

Share this map by copying the link below and sending it to friends and family:", "addToHomeScreen": "

Add to your home screen

You can easily add this website to your smartphone home screen for a native feel. Click the 'add to home screen button' in the URL bar to do this.", diff --git a/langs/layers/de.json b/langs/layers/de.json index 656df35020..3ed8abf070 100644 --- a/langs/layers/de.json +++ b/langs/layers/de.json @@ -1072,7 +1072,7 @@ } }, "tagRenderings": { - "1": { + "2": { "render": "Der Name dieses Bücherschrank lautet {name}", "question": "Wie heißt dieser öffentliche Bücherschrank?", "mappings": { @@ -1081,11 +1081,11 @@ } } }, - "2": { + "3": { "render": "{capacity} Bücher passen in diesen Bücherschrank", "question": "Wie viele Bücher passen in diesen öffentlichen Bücherschrank?" }, - "3": { + "4": { "question": "Welche Art von Büchern sind in diesem öffentlichen Bücherschrank zu finden?", "mappings": { "0": { @@ -1099,7 +1099,7 @@ } } }, - "4": { + "5": { "question": "Befindet sich dieser Bücherschrank im Freien?", "mappings": { "0": { @@ -1113,7 +1113,7 @@ } } }, - "5": { + "6": { "question": "Ist dieser öffentliche Bücherschrank frei zugänglich?", "mappings": { "0": { @@ -1124,11 +1124,11 @@ } } }, - "6": { + "7": { "question": "Wer unterhält diesen öffentlichen Bücherschrank?", "render": "Betrieben von {operator}" }, - "7": { + "8": { "question": "Ist dieser öffentliche Bücherschrank Teil eines größeren Netzwerks?", "render": "Dieser Bücherschrank ist Teil von {brand}", "mappings": { @@ -1140,7 +1140,7 @@ } } }, - "8": { + "9": { "render": "Die Referenznummer dieses öffentlichen Bücherschranks innerhalb {brand} lautet {ref}", "question": "Wie lautet die Referenznummer dieses öffentlichen Bücherschranks?", "mappings": { @@ -1149,11 +1149,11 @@ } } }, - "9": { + "10": { "question": "Wann wurde dieser öffentliche Bücherschrank installiert?", "render": "Installiert am {start_date}" }, - "10": { + "11": { "render": "Weitere Informationen auf der Webseite", "question": "Gibt es eine Website mit weiteren Informationen über diesen öffentlichen Bücherschrank?" } diff --git a/langs/layers/en.json b/langs/layers/en.json index 782a80eb75..24a6bfac60 100644 --- a/langs/layers/en.json +++ b/langs/layers/en.json @@ -1183,7 +1183,7 @@ } }, "tagRenderings": { - "1": { + "2": { "render": "The name of this bookcase is {name}", "question": "What is the name of this public bookcase?", "mappings": { @@ -1192,11 +1192,11 @@ } } }, - "2": { + "3": { "render": "{capacity} books fit in this bookcase", "question": "How many books fit into this public bookcase?" }, - "3": { + "4": { "question": "What kind of books can be found in this public bookcase?", "mappings": { "0": { @@ -1210,7 +1210,7 @@ } } }, - "4": { + "5": { "question": "Is this bookcase located outdoors?", "mappings": { "0": { @@ -1224,7 +1224,7 @@ } } }, - "5": { + "6": { "question": "Is this public bookcase freely accessible?", "mappings": { "0": { @@ -1235,11 +1235,11 @@ } } }, - "6": { + "7": { "question": "Who maintains this public bookcase?", "render": "Operated by {operator}" }, - "7": { + "8": { "question": "Is this public bookcase part of a bigger network?", "render": "This public bookcase is part of {brand}", "mappings": { @@ -1251,7 +1251,7 @@ } } }, - "8": { + "9": { "render": "The reference number of this public bookcase within {brand} is {ref}", "question": "What is the reference number of this public bookcase?", "mappings": { @@ -1260,11 +1260,11 @@ } } }, - "9": { + "10": { "question": "When was this public bookcase installed?", "render": "Installed on {start_date}" }, - "10": { + "11": { "render": "More info on the website", "question": "Is there a website with more information about this public bookcase?" } diff --git a/langs/layers/fr.json b/langs/layers/fr.json index 0f0a41d814..ccf1dc807d 100644 --- a/langs/layers/fr.json +++ b/langs/layers/fr.json @@ -1073,7 +1073,7 @@ } }, "tagRenderings": { - "1": { + "2": { "render": "Le nom de cette microbibliothèque est {name}", "question": "Quel est le nom de cette microbibliothèque ?", "mappings": { @@ -1082,11 +1082,11 @@ } } }, - "2": { + "3": { "render": "{capacity} livres peuvent entrer dans cette microbibliothèque", "question": "Combien de livres peuvent entrer dans cette microbibliothèque ?" }, - "3": { + "4": { "question": "Quel type de livres peut-on dans cette microbibliothèque ?", "mappings": { "0": { @@ -1100,7 +1100,7 @@ } } }, - "4": { + "5": { "question": "Cette microbiliothèque est-elle en extérieur ?", "mappings": { "0": { @@ -1114,7 +1114,7 @@ } } }, - "5": { + "6": { "question": "Cette microbibliothèque est-elle librement accèssible ?", "mappings": { "0": { @@ -1125,11 +1125,11 @@ } } }, - "6": { + "7": { "question": "Qui entretien cette microbibliothèque ?", "render": "Entretenue par {operator}" }, - "7": { + "8": { "question": "Cette microbibliothèque fait-elle partie d'un réseau/groupe ?", "render": "Cette microbibliothèque fait partie du groupe {brand}", "mappings": { @@ -1141,7 +1141,7 @@ } } }, - "8": { + "9": { "render": "Cette microbibliothèque du réseau {brand} possède le numéro {ref}", "question": "Quelle est le numéro de référence de cette microbibliothèque ?", "mappings": { @@ -1150,11 +1150,11 @@ } } }, - "9": { + "10": { "question": "Quand a été installée cette microbibliothèque ?", "render": "Installée le {start_date}" }, - "10": { + "11": { "render": "Plus d'infos sur le site web", "question": "Y a-t-il un site web avec plus d'informations sur cette microbibliothèque ?" } diff --git a/langs/layers/nl.json b/langs/layers/nl.json index 3fc2503625..8bc452325c 100644 --- a/langs/layers/nl.json +++ b/langs/layers/nl.json @@ -1361,7 +1361,7 @@ } }, "tagRenderings": { - "1": { + "2": { "render": "De naam van dit boekenruilkastje is {name}", "question": "Wat is de naam van dit boekenuilkastje?", "mappings": { @@ -1370,11 +1370,11 @@ } } }, - "2": { + "3": { "render": "Er passen {capacity} boeken", "question": "Hoeveel boeken passen er in dit boekenruilkastje?" }, - "3": { + "4": { "question": "Voor welke doelgroep zijn de meeste boeken in dit boekenruilkastje?", "mappings": { "0": { @@ -1388,7 +1388,7 @@ } } }, - "4": { + "5": { "question": "Staat dit boekenruilkastje binnen of buiten?", "mappings": { "0": { @@ -1402,7 +1402,7 @@ } } }, - "5": { + "6": { "question": "Is dit boekenruilkastje publiek toegankelijk?", "mappings": { "0": { @@ -1413,11 +1413,11 @@ } } }, - "6": { + "7": { "question": "Wie is verantwoordelijk voor dit boekenruilkastje?", "render": "Onderhouden door {operator}" }, - "7": { + "8": { "question": "Is dit boekenruilkastje deel van een netwerk?", "render": "Dit boekenruilkastje is deel van het netwerk {brand}", "mappings": { @@ -1429,7 +1429,7 @@ } } }, - "8": { + "9": { "render": "Het referentienummer binnen {brand} is {ref}", "question": "Wat is het referentienummer van dit boekenruilkastje?", "mappings": { @@ -1438,11 +1438,11 @@ } } }, - "9": { + "10": { "question": "Op welke dag werd dit boekenruilkastje geinstalleerd?", "render": "Geplaatst op {start_date}" }, - "10": { + "11": { "render": "Meer info op de website", "question": "Is er een website over dit boekenruilkastje?" } diff --git a/langs/layers/ru.json b/langs/layers/ru.json index 3aef434813..e22919e492 100644 --- a/langs/layers/ru.json +++ b/langs/layers/ru.json @@ -578,7 +578,7 @@ } }, "tagRenderings": { - "1": { + "2": { "render": "Название книжного шкафа — {name}", "question": "Как называется общественный книжный шкаф?", "mappings": { @@ -587,10 +587,10 @@ } } }, - "2": { + "3": { "question": "Сколько книг помещается в этом общественном книжном шкафу?" }, - "3": { + "4": { "mappings": { "0": { "then": "В основном детские книги" @@ -600,7 +600,7 @@ } } }, - "10": { + "11": { "render": "Более подробная информация на сайте" } } diff --git a/langs/themes/nl.json b/langs/themes/nl.json index 26c50fcf9c..92b057af46 100644 --- a/langs/themes/nl.json +++ b/langs/themes/nl.json @@ -239,7 +239,8 @@ } }, "campersite": { - "title": "Kampeersite" + "title": "Kampeersite", + "shortDescription": "Vind locaties waar je de nacht kan doorbrengen met je mobilehome" }, "climbing": { "title": "Open Klimkaart", @@ -368,10 +369,38 @@ "title": { "render": "Klimgelegenheid?" }, - "description": "Een klimgelegenheid?" + "description": "Een klimgelegenheid?", + "tagRenderings": { + "1": { + "mappings": { + "0": { + "then": "Klimmen is hier niet mogelijk" + }, + "1": { + "then": "Klimmen is hier niet toegelaten" + }, + "2": { + "then": "Klimmen is hier niet toegelaten" + } + } + } + } } }, "roamingRenderings": { + "0": { + "question": "Is er een (onofficiële) website met meer informatie (b.v. met topos)?" + }, + "1": { + "mappings": { + "0": { + "then": "Een omvattend element geeft aan dat dit publiek toegangkelijk is
{_embedding_feature:access:description}" + }, + "1": { + "then": "Een omvattend element geeft aan dat een toelating nodig is om hier te klimmen
{_embedding_feature:access:description}" + } + } + }, "4": { "render": "De klimroutes zijn gemiddeld {climbing:length}m lang", "question": "Wat is de (gemiddelde) lengte van de klimroutes, in meter?" diff --git a/package.json b/package.json index 75dcc75ac7..a1c2fc096e 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "reset:layeroverview": "echo {\\\"layers\\\":[], \\\"themes\\\":[]} > ./assets/generated/known_layers_and_themes.json", "generate": "mkdir -p ./assets/generated && npm run reset:layeroverview && npm run generate:images && npm run generate:translations && npm run generate:licenses && npm run generate:licenses && npm run generate:layeroverview", "build": "rm -rf dist/ && npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", - "prepare-deploy": "npm run generate:contributor-list && npm run generate && npm run test && npm run generate:editor-layer-index && npm run generate:layeroverview && npm run generate:layouts && npm run build && rm -rf .cache && npm run generate:docs", + "prepare-deploy": "npm run generate && npm run test && npm run generate:editor-layer-index && npm run generate:layeroverview && npm run generate:layouts && npm run build && rm -rf .cache && npm run generate:docs", "deploy:staging": "npm run prepare-deploy && rm -rf ~/git/pietervdvn.github.io/Staging/* && cp -r dist/* ~/git/pietervdvn.github.io/Staging/ && cd ~/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean", "deploy:pietervdvn": "cd ~/git/pietervdvn.github.io/ && git pull && cd - && npm run prepare-deploy && rm -rf ~/git/pietervdvn.github.io/MapComplete/* && cp -r dist/* ~/git/pietervdvn.github.io/MapComplete/ && cd ~/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean", "deploy:production": "cd ~/git/mapcomplete.github.io/ && git pull && cd - && rm -rf ./assets/generated && npm run prepare-deploy && npm run optimize-images && rm -rf ~/git/mapcomplete.github.io/* && cp -r dist/* ~/git/mapcomplete.github.io/ && cd ~/git/mapcomplete.github.io/ && echo \"mapcomplete.osm.be\" > CNAME && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean && npm run gittag", diff --git a/preferences.ts b/preferences.ts index 4b1dce30ab..1c1773a143 100644 --- a/preferences.ts +++ b/preferences.ts @@ -3,7 +3,6 @@ import Combine from "./UI/Base/Combine"; import {Button} from "./UI/Base/Button"; import {TextField} from "./UI/Input/TextField"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; -import {UIElement} from "./UI/UIElement"; import {UIEventSource} from "./Logic/UIEventSource"; import {Utils} from "./Utils"; import {SubtleButton} from "./UI/Base/SubtleButton"; diff --git a/scripts/ScriptUtils.ts b/scripts/ScriptUtils.ts index 220c9e2809..54b340a7cf 100644 --- a/scripts/ScriptUtils.ts +++ b/scripts/ScriptUtils.ts @@ -1,12 +1,23 @@ import {lstatSync, readdirSync, readFileSync} from "fs"; +import {Utils} from "../Utils"; +Utils.runningFromConsole = true import * as https from "https"; import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; +import * as fs from "fs"; + export default class ScriptUtils { + + + public static fixUtils() { + Utils.externalDownloadFunction = ScriptUtils.DownloadJSON + } + + public static readDirRecSync(path, maxDepth = 999): string[] { const result = [] - if(maxDepth <= 0){ + if (maxDepth <= 0) { return [] } for (const entry of readdirSync(path)) { @@ -23,6 +34,20 @@ export default class ScriptUtils { return result; } + public static DownloadFileTo(url, targetFilePath: string): void { + console.log("Downloading ", url, "to", targetFilePath) + https.get(url, (res) => { + const filePath = fs.createWriteStream(targetFilePath); + res.pipe(filePath); + filePath.on('finish', () => { + filePath.close(); + console.log('Download Completed'); + }) + + + }) + } + public static DownloadJSON(url): Promise { return new Promise((resolve, reject) => { try { @@ -77,7 +102,7 @@ export default class ScriptUtils { }) } - public static getThemeFiles() : {parsed: LayoutConfigJson, path: string}[] { + public static getThemeFiles(): { parsed: LayoutConfigJson, path: string }[] { return ScriptUtils.readDirRecSync("./assets/themes") .filter(path => path.endsWith(".json")) .filter(path => path.indexOf("license_info.json") < 0) diff --git a/scripts/fixTheme.ts b/scripts/fixTheme.ts index 991e56573a..bed224b746 100644 --- a/scripts/fixTheme.ts +++ b/scripts/fixTheme.ts @@ -1,15 +1,19 @@ - /* * This script attempt to automatically fix some basic issues when a theme from the custom generator is loaded */ import {Utils} from "../Utils" Utils.runningFromConsole = true; + import {readFileSync, writeFileSync} from "fs"; import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; -import {Layer} from "leaflet"; import LayerConfig from "../Customizations/JSON/LayerConfig"; import SmallLicense from "../Models/smallLicense"; import AllKnownLayers from "../Customizations/AllKnownLayers"; +import ScriptUtils from "./ScriptUtils"; +import AllImageProviders from "../Logic/ImageProviders/AllImageProviders"; + + +ScriptUtils.fixUtils() if(process.argv.length == 2){ console.log("USAGE: ts-node scripts/fixTheme ") @@ -23,7 +27,6 @@ console.log("Fixing up ", path) const themeConfigJson : LayoutConfigJson = JSON.parse(readFileSync(path, "UTF8")) -const linuxHints = [] const licenses : SmallLicense[] = [] const replacements: {source: string, destination: string}[] = [] @@ -41,15 +44,32 @@ for (const layerConfigJson of themeConfigJson.layers) { const layerConfig = new LayerConfig(layerConfigJson, AllKnownLayers.sharedUnits, "fix theme",true) const images : string[] = Array.from(layerConfig.ExtractImages()) const remoteImages = images.filter(img => img.startsWith("http")) + for (const remoteImage of remoteImages) { - linuxHints.push("wget " + remoteImage) + + + const filename = remoteImage.substring(remoteImage.lastIndexOf("/")) + ScriptUtils.DownloadFileTo(remoteImage, dir + "/" + filename) + + const imgPath = remoteImage.substring(remoteImage.lastIndexOf("/") + 1) - licenses.push({ - path: imgPath, - license: "", - authors: [], - sources: [remoteImage] - }) + + for (const attributionSrc of AllImageProviders.ImageAttributionSource) { + try { + attributionSrc.GetAttributionFor(remoteImage).addCallbackAndRun(license => { + console.log("Downloaded an attribution!") + licenses.push({ + path: imgPath, + license: license?.license ?? "", + authors:Utils.NoNull([license?.artist]), + sources: [remoteImage] + }) + }) + }catch(e){ + // Hush hush + } + } + replacements.push({source: remoteImage, destination: `${dir}/${imgPath}`}) } } @@ -59,13 +79,9 @@ for (const replacement of replacements) { fixedThemeJson = fixedThemeJson.replace(new RegExp(replacement.source, "g"), replacement.destination) } -const fixScriptPath = dir + "/fix_script_"+path.replace(/\//g,"_")+".sh" writeFileSync(dir + "/generated.license_info.json", JSON.stringify(licenses, null, " ")) -writeFileSync(fixScriptPath, linuxHints.join("\n")) writeFileSync(path+".autofixed.json", fixedThemeJson) console.log(`IMPORTANT: - 1) run ${fixScriptPath} - 2) Copy generated.license_info.json over into license_info.json and add the missing attributions and authors - 3) Verify ${path}.autofixed.json as theme, and rename it to ${path} - 4) Delete the fix script and other unneeded files`) \ No newline at end of file + 1) Copy generated.license_info.json over into license_info.json and add the missing attributions and authors + 2) Verify ${path}.autofixed.json as theme, and rename it to ${path}`) \ No newline at end of file diff --git a/scripts/generateLayerOverview.ts b/scripts/generateLayerOverview.ts index db051e27e4..ed4df23ed2 100644 --- a/scripts/generateLayerOverview.ts +++ b/scripts/generateLayerOverview.ts @@ -1,8 +1,5 @@ import ScriptUtils from "./ScriptUtils"; -import {Utils} from "../Utils"; -import {readFileSync, writeFileSync} from "fs"; - -Utils.runningFromConsole = true +import {writeFileSync} from "fs"; import LayerConfig from "../Customizations/JSON/LayerConfig"; import * as licenses from "../assets/generated/license_info.json" import LayoutConfig from "../Customizations/JSON/LayoutConfig"; @@ -10,6 +7,7 @@ import {LayerConfigJson} from "../Customizations/JSON/LayerConfigJson"; import {Translation} from "../UI/i18n/Translation"; import {LayoutConfigJson} from "../Customizations/JSON/LayoutConfigJson"; import AllKnownLayers from "../Customizations/AllKnownLayers"; + // This scripts scans 'assets/layers/*.json' for layer definition files and 'assets/themes/*.json' for theme definition files. // It spits out an overview of those to be used to load them diff --git a/test.html b/test.html index 839114efe4..8b6c44878d 100644 --- a/test.html +++ b/test.html @@ -3,6 +3,7 @@ Small tests + diff --git a/test.ts b/test.ts index 373849c40c..8ae5dc012c 100644 --- a/test.ts +++ b/test.ts @@ -9,12 +9,16 @@ import {SlideShow} from "./UI/Image/SlideShow"; import {FixedUiElement} from "./UI/Base/FixedUiElement"; import Img from "./UI/Base/Img"; import {AttributedImage} from "./UI/Image/AttributedImage"; -import {Imgur} from "./Logic/Web/Imgur"; -import ReviewForm from "./UI/Reviews/ReviewForm"; -import {OsmConnection} from "./Logic/Osm/OsmConnection"; +import {Imgur} from "./Logic/ImageProviders/Imgur"; +import Minimap from "./UI/Base/Minimap"; +import Loc from "./Models/Loc"; +import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers"; +import ShowDataLayer from "./UI/ShowDataLayer"; +import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; -function TestSlideshow(){ +function TestSlideshow() { const elems = new UIEventSource([ new FixedUiElement("A"), new FixedUiElement("qmsldkfjqmlsdkjfmqlskdjfmqlksdf").SetClass("text-xl"), @@ -25,17 +29,17 @@ function TestSlideshow(){ new SlideShow(elems).AttachTo("maindiv") } -function TestTagRendering(){ +function TestTagRendering() { State.state = new State(undefined) const tagsSource = new UIEventSource({ - id:"node/1" + id: "node/1" }) new TagRenderingQuestion( tagsSource, new TagRenderingConfig({ multiAnswer: false, freeform: { - key:"valve" + key: "valve" }, question: "What valves are supported?", render: "This pump supports {valve}", @@ -45,8 +49,8 @@ function TestTagRendering(){ then: "This pump supports dunlop" }, { - if:"valve=shrader", - then:"shrader is supported", + if: "valve=shrader", + then: "shrader is supported", } ], @@ -56,15 +60,84 @@ function TestTagRendering(){ new VariableUiElement(tagsSource.map(tags => tags["valves"])).SetClass("alert").AttachTo("extradiv") } -function TestAllInputMethods(){ +function TestAllInputMethods() { new Combine(ValidatedTextField.tpList.map(tp => { const tf = ValidatedTextField.InputForType(tp.name); return new Combine([tf, new VariableUiElement(tf.GetValue()).SetClass("alert")]); - })).AttachTo("maindiv") + })).AttachTo("maindiv") } -new ReviewForm(() => { - return undefined; -}, new OsmConnection(true, new UIEventSource(undefined), "test")).AttachTo("maindiv"); \ No newline at end of file + +const location = new UIEventSource({ + lon: 4.84771728515625, + lat: 51.17920846421931, + zoom: 14 +}) +const map0 = new Minimap({ + location: location, + allowMoving: true, + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[2]) +}) +map0.SetStyle("width: 500px; height: 250px; overflow: hidden; border: 2px solid red") + .AttachTo("maindiv") + +const layout = AllKnownLayouts.layoutsList[1] +State.state = new State(layout) +console.log("LAYOUT is", layout.id) + +const feature = { + "type": "Feature", + _matching_layer_id: "bike_repair_station", + "properties": { + id: "node/-1", + "amenity": "bicycle_repair_station" + }, + "geometry": { + "type": "Point", + "coordinates": [ + 4.84771728515625, + 51.17920846421931 + ] + } + } + +; + +State.state.allElements.addOrGetElement(feature) + +const featureSource = new UIEventSource([{ + freshness: new Date(), + feature: feature +}]) + +new ShowDataLayer( + featureSource, + map0.leafletMap, + new UIEventSource(layout) +) + +const map1 = new Minimap({ + location: location, + allowMoving: true, + background: new AvailableBaseLayers(location).availableEditorLayers.map(layers => layers[5]) + }, +) + +map1.SetStyle("width: 500px; height: 250px; overflow: hidden; border : 2px solid black") + .AttachTo("extradiv") + + + + + +new ShowDataLayer( + featureSource, + map1.leafletMap, + new UIEventSource(layout) +) + +featureSource.ping() + +// */ \ No newline at end of file