diff --git a/Customizations/AllKnownLayouts.ts b/Customizations/AllKnownLayouts.ts index 348d543..2772468 100644 --- a/Customizations/AllKnownLayouts.ts +++ b/Customizations/AllKnownLayouts.ts @@ -16,10 +16,11 @@ import * as charging_stations from "../assets/themes/charging_stations/charging_ import * as widths from "../assets/themes/widths/width.json" import * as drinking_water from "../assets/themes/drinking_water/drinking_water.json" import * as climbing from "../assets/themes/climbing/climbing.json" -import LayerConfig from "./JSON/LayerConfig"; -import SharedLayers from "./SharedLayers"; +import * as surveillance_cameras from "../assets/themes/surveillance_cameras/surveillance_cameras.json" import * as personal from "../assets/themes/personalLayout/personalLayout.json" +import LayerConfig from "./JSON/LayerConfig"; import LayoutConfig from "./JSON/LayoutConfig"; +import SharedLayers from "./SharedLayers"; export class AllKnownLayouts { @@ -62,6 +63,7 @@ export class AllKnownLayouts { new LayoutConfig(buurtnatuur), new LayoutConfig(bike_monitoring_stations), new LayoutConfig(climbing), + new LayoutConfig(surveillance_cameras) ]; diff --git a/Customizations/JSON/LayerConfig.ts b/Customizations/JSON/LayerConfig.ts index 7b610b1..6e14be6 100644 --- a/Customizations/JSON/LayerConfig.ts +++ b/Customizations/JSON/LayerConfig.ts @@ -8,23 +8,33 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson"; import {Translation} from "../../UI/i18n/Translation"; import {Img} from "../../UI/Img"; import Svg from "../../Svg"; +import {SubstitutedTranslation} from "../../UI/SpecialVisualizations"; +import {Utils} from "../../Utils"; +import Combine from "../../UI/Base/Combine"; +import {VariableUiElement} from "../../UI/Base/VariableUIElement"; export default class LayerConfig { + + id: string; name: Translation description: Translation; overpassTags: TagsFilter; + doNotDownload: boolean; + + passAllFeatures: boolean; minzoom: number; - title: TagRenderingConfig; + title?: TagRenderingConfig; titleIcons: TagRenderingConfig[]; icon: TagRenderingConfig; iconSize: TagRenderingConfig; + rotation: TagRenderingConfig; color: TagRenderingConfig; width: TagRenderingConfig; dashArray: TagRenderingConfig; @@ -46,17 +56,19 @@ export default class LayerConfig { tagRenderings: TagRenderingConfig []; - constructor(json: LayerConfigJson, context?: string) { + constructor(json: LayerConfigJson, roamingRenderings: TagRenderingConfig[], + context?: string) { context = context + "." + json.id; this.id = json.id; this.name = Translations.T(json.name); this.description = Translations.T(json.name); this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags"); + this.doNotDownload = json.doNotDownload ?? false, + this.passAllFeatures = json.passAllFeatures ?? false; this.minzoom = json.minzoom; this.wayHandling = json.wayHandling ?? 0; this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0; - this.title = new TagRenderingConfig(json.title); this.presets = (json.presets ?? []).map(pr => ({ title: Translations.T(pr.title), @@ -87,13 +99,16 @@ export default class LayerConfig { }); } - this.tagRenderings = trs(json.tagRenderings); + this.tagRenderings = trs(json.tagRenderings).concat(roamingRenderings); this.titleIcons = trs(json.titleIcons ?? ["wikipedialink","osmlink"]); function tr(key, deflt) { const v = json[key]; - if (v === undefined) { + if (v === undefined || v === null) { + if (deflt === undefined) { + return undefined; + } return new TagRenderingConfig(deflt); } if (typeof v === "string") { @@ -107,13 +122,120 @@ export default class LayerConfig { } - this.title = tr("title", ""); + this.title = tr("title", undefined); this.icon = tr("icon", Img.AsData(Svg.bug)); + const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt; + if (iconPath.startsWith(Utils.assets_path)) { + const iconKey = iconPath.substr(Utils.assets_path.length); + if (Svg.All[iconKey] === undefined) { + throw "Builtin SVG asset not found: " + iconPath + } + } this.iconSize = tr("iconSize", "40,40,center"); this.color = tr("color", "#0000ff"); this.width = tr("width", "7"); + this.rotation = tr("rotation", "0"); this.dashArray = tr("dashArray", ""); } + + + public GenerateLeafletStyle(tags: any, clickable: boolean): + { + color: string; + icon: { + iconUrl: string, + popupAnchor: [number, number]; + iconAnchor: [number, number]; + iconSize: [number, number]; + html: string; + rotation: string; + className?: string; + }; + weight: number; dashArray: number[] + } { + + function num(str, deflt = 40) { + const n = Number(str); + if (isNaN(n)) { + return deflt; + } + return n; + } + + function rendernum(tr: TagRenderingConfig, deflt: number) { + const str = Number(render(tr, "" + deflt)); + const n = Number(str); + if (isNaN(n)) { + return deflt; + } + return n; + } + + function render(tr: TagRenderingConfig, deflt?: string) { + const str = (tr?.GetRenderValue(tags)?.txt ?? deflt); + return SubstitutedTranslation.SubstituteKeys(str, tags); + } + + const iconUrl = render(this.icon); + const iconSize = render(this.iconSize, "40,40,center").split(","); + const dashArray = render(this.dashArray).split(" ").map(Number); + let color = render(this.color, "#00f"); + + if (color.startsWith("--")) { + color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") + } + + const weight = rendernum(this.width, 5); + const rotation = render(this.rotation, "0deg"); + + + const iconW = num(iconSize[0]); + const iconH = num(iconSize[1]); + const mode = iconSize[2] ?? "center" + + let anchorW = iconW / 2; + let anchorH = iconH / 2; + if (mode === "left") { + anchorW = 0; + } + if (mode === "right") { + anchorW = iconW; + } + + if (mode === "top") { + anchorH = 0; + } + if (mode === "bottom") { + anchorH = iconH; + } + + + let html = ``; + + if (iconUrl.startsWith(Utils.assets_path)) { + const key = iconUrl.substr(Utils.assets_path.length); + html = new Combine([ + (Svg.All[key] as string).replace(/stop-color:#000000/g, 'stop-color:' + color) + ]).SetStyle(`width:100%;height:100%;rotate:${rotation};display:block;`).Render(); + } + return { + icon: + { + html: html, + iconSize: [iconW, iconH], + iconAnchor: [anchorW, anchorH], + popupAnchor: [0, 3 - anchorH], + rotation: rotation, + iconUrl: iconUrl, + className: clickable ? "leaflet-div-icon" : "leaflet-div-icon unclickable" + }, + color: color, + weight: weight, + dashArray: dashArray + }; + } + + } \ No newline at end of file diff --git a/Customizations/JSON/LayerConfigJson.ts b/Customizations/JSON/LayerConfigJson.ts index be70638..98df035 100644 --- a/Customizations/JSON/LayerConfigJson.ts +++ b/Customizations/JSON/LayerConfigJson.ts @@ -29,6 +29,12 @@ export interface LayerConfigJson { */ overpassTags: AndOrTagConfigJson | string; + /** + * If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers. + * Works well together with 'passAllFeatures', to add decoration + */ + doNotDownload?: boolean; + /** * The zoomlevel at which point the data is shown and loaded. */ @@ -39,8 +45,13 @@ export interface LayerConfigJson { /** * The title shown in a popup for elements of this layer. */ - title: string | TagRenderingConfigJson; - + title?: string | TagRenderingConfigJson; + + /** + * Small icons shown next to the title. + * If not specified, the OsmLink and wikipedia links will be used by default. + * Use an empty array to hide them + */ titleIcons?: (string | TagRenderingConfigJson)[]; /** @@ -54,9 +65,15 @@ export interface LayerConfigJson { * Default is '40,40,center' */ iconSize?: string | TagRenderingConfigJson; - /** - * The color for way-elements + * The rotation of an icon, useful for e.g. directions. + * Usage: as if it were a css property for 'rotate', thus has to end with 'deg', e.g. `90deg`, `{direction}deg`, `calc(90deg - {camera:direction}deg)`` + */ + rotation?: string | TagRenderingConfigJson; + + /** + * The color for way-elements and SVG-elements. + * If the value starts with "--", the style of the body element will be queried for the corresponding variable instead */ color?: string | TagRenderingConfigJson; /** @@ -87,6 +104,11 @@ export interface LayerConfigJson { */ hideUnderlayingFeaturesMinPercentage?:number; + /** + * If set, this layer will pass all the features it receives onto the next layer + */ + passAllFeatures?:boolean + /** * Presets for this layer */ @@ -98,6 +120,7 @@ export interface LayerConfigJson { /** * All the tag renderings. + * A tag rendering is a block that either shows the known value or asks a question. */ tagRenderings?: (string | TagRenderingConfigJson) [] } \ No newline at end of file diff --git a/Customizations/JSON/LayoutConfig.ts b/Customizations/JSON/LayoutConfig.ts index 2a22ccd..b914a79 100644 --- a/Customizations/JSON/LayoutConfig.ts +++ b/Customizations/JSON/LayoutConfig.ts @@ -80,7 +80,7 @@ export default class LayoutConfig { } else { throw "Unkown fixed layer " + layer; } - return new LayerConfig(layer, `${this.id}.layers[${i}]`); + return new LayerConfig(layer, this.roamingRenderings,`${this.id}.layers[${i}]`); }); this.hideFromOverview = json.hideFromOverview ?? false; diff --git a/Customizations/JSON/LayoutConfigJson.ts b/Customizations/JSON/LayoutConfigJson.ts index 95c14c4..fe5451f 100644 --- a/Customizations/JSON/LayoutConfigJson.ts +++ b/Customizations/JSON/LayoutConfigJson.ts @@ -102,7 +102,19 @@ export interface LayoutConfigJson { /** - * The layers to display + * The layers to display. + * + * Every layer contains a description of which feature to display - the overpassTags which are queried. + * Instead of running one query for every layer, the query is fused. + * + * Afterwards, every layer is given the list of features. + * Every layer takes away the features that match with them*, and give the leftovers to the next layers. + * + * This implies that the _order_ of the layers is important in the case of features with the same tags; + * as the later layers might never receive their feature. + * + * *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself + * */ layers: (LayerConfigJson | string)[], diff --git a/Customizations/SharedLayers.ts b/Customizations/SharedLayers.ts index 34dd9da..4beaa3e 100644 --- a/Customizations/SharedLayers.ts +++ b/Customizations/SharedLayers.ts @@ -13,6 +13,8 @@ import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json" import * as bike_cleaning from "../assets/layers/bike_cleaning/bike_cleaning.json" import * as maps from "../assets/layers/maps/maps.json" import * as information_boards from "../assets/layers/information_board/information_board.json" +import * as direction from "../assets/layers/direction/direction.json" +import * as surveillance_camera from "../assets/layers/surveillance_cameras/surveillance_cameras.json" import LayerConfig from "./JSON/LayerConfig"; export default class SharedLayers { @@ -24,20 +26,22 @@ export default class SharedLayers { private static getSharedLayers(){ const sharedLayersList = [ - new LayerConfig(drinkingWater, "shared_layers"), - new LayerConfig(ghostbikes, "shared_layers"), - new LayerConfig(viewpoint, "shared_layers"), - new LayerConfig(bike_parking, "shared_layers"), - new LayerConfig(bike_repair_station, "shared_layers"), - new LayerConfig(bike_monitoring_station, "shared_layers"), - new LayerConfig(birdhides, "shared_layers"), - new LayerConfig(nature_reserve, "shared_layers"), - new LayerConfig(bike_cafes, "shared_layers"), - new LayerConfig(cycling_themed_objects, "shared_layers"), - new LayerConfig(bike_shops, "shared_layers"), - new LayerConfig(bike_cleaning, "shared_layers"), - new LayerConfig(maps, "shared_layers"), - new LayerConfig(information_boards, "shared_layers") + new LayerConfig(drinkingWater,[], "shared_layers"), + new LayerConfig(ghostbikes,[], "shared_layers"), + new LayerConfig(viewpoint,[], "shared_layers"), + new LayerConfig(bike_parking,[], "shared_layers"), + new LayerConfig(bike_repair_station,[], "shared_layers"), + new LayerConfig(bike_monitoring_station,[], "shared_layers"), + new LayerConfig(birdhides,[], "shared_layers"), + new LayerConfig(nature_reserve,[], "shared_layers"), + new LayerConfig(bike_cafes,[], "shared_layers"), + new LayerConfig(cycling_themed_objects,[], "shared_layers"), + new LayerConfig(bike_shops,[], "shared_layers"), + new LayerConfig(bike_cleaning,[], "shared_layers"), + new LayerConfig(maps,[], "shared_layers"), + new LayerConfig(direction,[], "shared_layers"), + new LayerConfig(information_boards,[], "shared_layers"), + new LayerConfig(surveillance_camera,[], "shared_layers") ]; const sharedLayers = new Map(); diff --git a/InitUiElements.ts b/InitUiElements.ts index 4b9a855..e4ac702 100644 --- a/InitUiElements.ts +++ b/InitUiElements.ts @@ -35,6 +35,9 @@ import Svg from "./Svg"; import Link from "./UI/Base/Link"; import * as personal from "./assets/themes/personalLayout/personalLayout.json" import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import * as L from "leaflet"; +import {Img} from "./UI/Img"; +import {UserDetails} from "./Logic/Osm/OsmConnection"; export class InitUiElements { @@ -110,6 +113,9 @@ export class InitUiElements { InitUiElements.setupAllLayerElements(); + if (layoutToUse.customCss !== undefined) { + Utils.LoadCustomCss(layoutToUse.customCss); + } function updateFavs() { const favs = State.state.favouriteLayers.data ?? []; @@ -139,6 +145,7 @@ export class InitUiElements { } + if (layoutToUse.id === personal.id) { State.state.favouriteLayers.addCallback(updateFavs); State.state.installedThemes.addCallback(updateFavs); @@ -150,27 +157,36 @@ export class InitUiElements { * This is given to the div which renders fullscreen on mobile devices */ State.state.selectedElement.addCallback((feature) => { - if (feature?.feature?.properties === undefined) { + + if (feature === undefined) { + State.state.fullScreenMessage.setData(undefined); + } + if (feature?.properties === undefined) { return; } - const data = feature.feature.properties; + const data = feature.properties; // Which is the applicable set? for (const layer of layoutToUse.layers) { if (typeof layer === "string") { continue; } const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data)); - if (applicable) { - // This layer is the layer that gives the questions - - const featureBox = new FeatureInfoBox( - State.state.allElements.getElement(data.id), - layer - ); - - State.state.fullScreenMessage.setData(featureBox); - break; + if (!applicable) { + continue; } + + if(layer.title === null && layer.tagRenderings.length === 0){ + continue; + } + + // This layer is the layer that gives the questions + const featureBox = new FeatureInfoBox( + State.state.allElements.getElement(data.id), + layer + ); + + State.state.fullScreenMessage.setData(featureBox); + break; } } ); @@ -201,6 +217,21 @@ export class InitUiElements { content.AttachTo("messagesbox"); } + State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home) + .addCallbackAndRun(home => { + if (home === undefined) { + return; + } + const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color") + const icon = L.icon({ + iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)), + iconSize: [30, 30], + iconAnchor: [15, 15] + }); + const marker = L.marker([home.lat, home.lon], {icon: icon}) + marker.addTo(State.state.bm.map) + console.log(marker) + }); new GeoLocationHandler() .SetStyle(`position:relative;display:block;border: solid 2px #0005;cursor: pointer; z-index: 999; /*Just below leaflets zoom*/background-color: white;border-radius: 5px;width: 43px;height: 43px;`) @@ -269,7 +300,7 @@ export class InitUiElements { ] if (State.state.featureSwitchShareScreen.data) { - tabs.push({header: Svg.share_img, content: new ShareScreen()}); + tabs.push({header: Svg.share, content: new ShareScreen()}); } if (State.state.featureSwitchMoreQuests.data) { @@ -282,12 +313,12 @@ export class InitUiElements { tabs.push({ - header: Svg.help_img, + header: Svg.help , content: new VariableUiElement(State.state.osmConnection.userDetails.map(userdetails => { if (userdetails.csCount < State.userJourney.mapCompleteHelpUnlock) { return "" } - return Translations.t.general.aboutMapcomplete.Render(); + return new Combine([Translations.t.general.aboutMapcomplete, "
Version "+State.vNumber]).Render(); }, [Locale.language])) } ); @@ -303,8 +334,8 @@ export class InitUiElements { const fullOptions = this.CreateWelcomePane(); - const help = Svg.help_ui().SetClass("open-welcome-button"); - const close = Svg.close_ui().SetClass("close-welcome-button"); + const help = Svg.help_svg().SetClass("open-welcome-button"); + const close = Svg.close_svg().SetClass("close-welcome-button"); const checkbox = new CheckBox( new Combine([ close, @@ -324,11 +355,15 @@ export class InitUiElements { checkbox.isEnabled.setData(false); }) + State.state.selectedElement.addCallback(() => { + checkbox.isEnabled.setData(false); + }) + const fullOptions2 = this.CreateWelcomePane(); State.state.fullScreenMessage.setData(fullOptions2) - Svg.help_ui() + Svg.help_svg() .SetClass("open-welcome-button") .SetClass("shadow") .onClick(() => { @@ -365,15 +400,15 @@ export class InitUiElements { return; } - layerControlPanel.SetStyle("display:block;padding:1em;border-radius:1em;"); - const closeButton = Svg.close_ui().SetClass("layer-selection-toggle").SetStyle(" background: #e5f5ff;") + layerControlPanel.SetStyle("display:block;padding:0.75em;border-radius:1em;"); + const closeButton = Svg.close_svg().SetClass("layer-selection-toggle").SetStyle(" background: var(--subtle-detail-color);") const checkbox = new CheckBox( new Combine([ closeButton, layerControlPanel]).SetStyle("display:flex;flex-direction:row;") .SetClass("hidden-on-mobile") , - Svg.layers_ui().SetClass("layer-selection-toggle"), + Svg.layers_svg().SetClass("layer-selection-toggle"), State.state.layerControlIsOpened ); @@ -432,17 +467,19 @@ export class InitUiElements { return new Combine([mapComplete, reportBug, " | ", stats, " | ", editHere, editWithJosm]).Render(); }, [State.state.osmConnection.userDetails]) - ).SetClass("map-attribution") } static InitBaseMap() { const bm = new Basemap("leafletDiv", State.state.locationControl, this.CreateAttribution()); State.state.bm = bm; + bm.map.on("popupclose", () => { + State.state.selectedElement.setData(undefined) + }) State.state.layerUpdater = new UpdateFromOverpass(State.state); State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state).availableEditorLayers; - const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackgroundId); + const queryParam = QueryParameters.GetQueryParameter("background", State.state.layoutToUse.data.defaultBackgroundId, "The id of the background layer to start with"); queryParam.addCallbackAndRun((selectedId: string) => { const available = State.state.availableBackgroundLayers.data; @@ -472,18 +509,15 @@ export class InitUiElements { throw "Layer " + layer + " was not substituted"; } - const generateInfo = (tagsES, feature) => { + let generateContents = (tags: UIEventSource) => new FeatureInfoBox(tags, layer); + if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) { + generateContents = undefined; + } - return new FeatureInfoBox( - tagsES, - layer, - ) - }; - - const flayer: FilteredLayer = FilteredLayer.fromDefinition(layer, generateInfo); + const flayer: FilteredLayer = new FilteredLayer(layer, generateContents); flayers.push(flayer); - QueryParameters.GetQueryParameter("layer-" + layer.id, "true") + QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown") .map((str) => str !== "false", [], (b) => b.toString()) .syncWith( flayer.isDisplayed diff --git a/Logic/FilteredLayer.ts b/Logic/FilteredLayer.ts index bcf71c4..c978185 100644 --- a/Logic/FilteredLayer.ts +++ b/Logic/FilteredLayer.ts @@ -6,6 +6,7 @@ import {GeoOperations} from "./GeoOperations"; import {UIElement} from "../UI/UIElement"; import State from "../State"; import LayerConfig from "../Customizations/JSON/LayerConfig"; +import Hash from "./Web/Hash"; /*** * A filtered layer is a layer which offers a 'set-data' function @@ -25,13 +26,9 @@ export class FilteredLayer { public readonly layerDef: LayerConfig; private readonly _maxAllowedOverlap: number; - private readonly _style: (properties) => { color: string, weight?: number, icon: { iconUrl: string, iconSize?: [number, number], popupAnchor?: [number, number], iconAnchor?: [number, number] } }; - - /** The featurecollection from overpass */ private _dataFromOverpass: any[]; - private readonly _wayHandling: number; /** List of new elements, geojson features */ private _newElements = []; @@ -49,60 +46,7 @@ export class FilteredLayer { ) { this.layerDef = layerDef; - this._wayHandling = layerDef.wayHandling; this._showOnPopup = showOnPopup; - this._style = (tags) => { - - const iconUrl = layerDef.icon?.GetRenderValue(tags)?.txt; - const iconSize = (layerDef.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(","); - - - const dashArray = layerDef.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number); - - function num(str, deflt = 40) { - const n = Number(str); - if (isNaN(n)) { - return deflt; - } - return n; - } - - const iconW = num(iconSize[0]); - const iconH = num(iconSize[1]); - const mode = iconSize[2] ?? "center" - - let anchorW = iconW / 2; - let anchorH = iconH / 2; - if (mode === "left") { - anchorW = 0; - } - if (mode === "right") { - anchorW = iconW; - } - - if (mode === "top") { - anchorH = 0; - } - if (mode === "bottom") { - anchorH = iconH; - } - - - const color = layerDef.color?.GetRenderValue(tags)?.txt ?? "#00f"; - let weight = num(layerDef.width?.GetRenderValue(tags)?.txt, 5); - return { - icon: - { - iconUrl: iconUrl, - iconSize: [iconW, iconH], - iconAnchor: [anchorW, anchorH], - popupAnchor: [0, 3 - anchorH] - }, - color: color, - weight: weight, - dashArray: dashArray - }; - }; this.name = name; this.filters = layerDef.overpassTags; this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage; @@ -123,17 +67,6 @@ export class FilteredLayer { } }) } - - static fromDefinition( - definition, - showOnPopup: (tags: UIEventSource, feature: any) => UIElement): - FilteredLayer { - return new FilteredLayer( - definition, showOnPopup); - - } - - /** * The main function to load data into this layer. * The data that is NOT used by this layer, is returned as a geojson object; the other data is rendered @@ -142,32 +75,17 @@ export class FilteredLayer { const leftoverFeatures = []; const selfFeatures = []; for (let feature of geojson.features) { - // feature.properties contains all the properties - const tags = TagUtils.proprtiesToKV(feature.properties); - - if (!this.filters.matches(tags)) { + const matches = this.filters.matches(tags); + if (matches) { + selfFeatures.push(feature); + } + if (!matches || this.layerDef.passAllFeatures) { leftoverFeatures.push(feature); - continue; } - - if (feature.geometry.type !== "Point") { - const centerPoint = GeoOperations.centerpoint(feature); - if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { - selfFeatures.push(centerPoint); - } else if (this._wayHandling === LayerConfig.WAYHANDLING_CENTER_ONLY) { - feature = centerPoint; - } - } - selfFeatures.push(feature); - } - - this.RenderLayer({ - type: "FeatureCollection", - features: selfFeatures - }) + this.RenderLayer(selfFeatures) const notShadowed = []; for (const feature of leftoverFeatures) { @@ -190,18 +108,126 @@ export class FilteredLayer { public AddNewElement(element) { this._newElements.push(element); - this.RenderLayer({features: this._dataFromOverpass}, element); // Update the layer - + this.RenderLayer( this._dataFromOverpass); // Update the layer } - private RenderLayer(data, openPopupOf = undefined) { - let self = this; + private RenderLayer(features) { if (this._geolayer !== undefined && this._geolayer !== null) { // Remove the old geojson layer from the map - we'll reshow all the elements later on anyway State.state.bm.map.removeLayer(this._geolayer); } + // We fetch all the data we have to show: + let fusedFeatures = this.ApplyWayHandling(this.FuseData(features)); + + // And we copy some features as points - if needed + const data = { + type: "FeatureCollection", + features: fusedFeatures + } + + let self = this; + this._geolayer = L.geoJSON(data, { + style: feature => + self.layerDef.GenerateLeafletStyle(feature.properties, self._showOnPopup !== undefined), + pointToLayer: function (feature, latLng) { + // Point to layer converts the 'point' to a layer object - as the geojson layer natively cannot handle points + // Click handling is done in the next step + + const style = self.layerDef.GenerateLeafletStyle(feature.properties, self._showOnPopup !== undefined); + let marker; + if (style.icon === undefined) { + marker = L.circle(latLng, { + radius: 25, + color: style.color + }); + } else if (style.icon.iconUrl.startsWith("$circle")) { + marker = L.circle(latLng, { + radius: 25, + color: style.color + }); + } else { + marker = L.marker(latLng, { + icon: L.divIcon(style.icon) + }); + } + return marker; + }, + onEachFeature: function (feature, layer: Layer) { + + if (self._showOnPopup === undefined) { + // No popup contents defined -> don't do anything + return; + } + const popup = L.popup({ + autoPan: true, + closeOnEscapeKey: true, + }, layer); + + let uiElement: UIElement; + + const eventSource = State.state.allElements.addOrGetElement(feature); + uiElement = self._showOnPopup(eventSource, feature); + popup.setContent(uiElement.Render()); + layer.bindPopup(popup); + // We first render the UIelement (which'll still need an update later on...) + // But at least it'll be visible already + + + layer.on("click", (e) => { + // We set the element as selected... + State.state.selectedElement.setData(feature); + uiElement.Update(); + }); + + if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) { + const center = GeoOperations.centerpoint(feature).geometry.coordinates; + popup.setLatLng({lat: center[1], lng: center[0]}); + popup.openOn(State.state.bm.map) + } + + } + }); + + if (this.combinedIsDisplayed.data) { + this._geolayer.addTo(State.state.bm.map); + } + + } + + private ApplyWayHandling(fusedFeatures: any[]) { + if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_DEFAULT) { + // We don't have to do anything special + return fusedFeatures; + } + + + // We have to convert all the ways into centerpoints + const existingPoints = []; + const newPoints = []; + const existingWays = []; + + for (const feature of fusedFeatures) { + if (feature.geometry.type === "Point") { + existingPoints.push(feature); + continue; + } + + existingWays.push(feature); + const centerPoint = GeoOperations.centerpoint(feature); + newPoints.push(centerPoint); + } + + fusedFeatures = existingPoints.concat(newPoints); + if (this.layerDef.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY) { + fusedFeatures = fusedFeatures.concat(existingWays) + } + return fusedFeatures; + } + + //*Fuses the old and the new datasets*/ + private FuseData(data: any[]) { const oldData = this._dataFromOverpass ?? []; // We keep track of all the ids that are freshly loaded in order to avoid adding duplicates @@ -209,7 +235,7 @@ export class FilteredLayer { // A list of all the features to show const fusedFeatures = []; // First, we add all the fresh data: - for (const feature of data.features) { + for (const feature of data) { idsFromOverpass.add(feature.properties.id); fusedFeatures.push(feature); } @@ -230,132 +256,6 @@ export class FilteredLayer { fusedFeatures.push(feature); } } - - - // We use a new, fused dataset - data = { - type: "FeatureCollection", - features: fusedFeatures - } - - - // The data is split in two parts: the poinst and the rest - // The points get a special treatment in order to render them properly - // Note that some features might get a point representation as well - - const runWhenAdded: (() => void)[] = [] - - this._geolayer = L.geoJSON(data, { - style: function (feature) { - return self._style(feature.properties); - }, - pointToLayer: function (feature, latLng) { - const style = self._style(feature.properties); - let marker; - if (style.icon === undefined) { - marker = L.circle(latLng, { - radius: 25, - color: style.color - }); - - } else if (style.icon.iconUrl.startsWith("$circle")) { - marker = L.circle(latLng, { - radius: 25, - color: style.color - }); - } else { - if (style.icon.iconSize === undefined) { - style.icon.iconSize = [50, 50] - } - - // @ts-ignore - marker = L.marker(latLng, { - icon: L.icon(style.icon), - }); - } - let eventSource = State.state.allElements.addOrGetElement(feature); - const popup = L.popup({}, marker); - let uiElement: UIElement; - let content = undefined; - let p = marker.bindPopup(popup) - .on("popupopen", () => { - if (content === undefined) { - uiElement = self._showOnPopup(eventSource, feature); - // Lazily create the content - content = uiElement.Render(); - } - popup.setContent(content); - uiElement.Update(); - }); - - if (feature === openPopupOf) { - runWhenAdded.push(() => { - p.openPopup(); - }) - } - - return marker; - }, - - onEachFeature: function (feature, layer:Layer) { - - // We monky-patch the feature element with an update-style - function updateStyle () { - // @ts-ignore - if (layer.setIcon) { - const style = self._style(feature.properties); - const icon = style.icon; - if (icon.iconUrl) { - if (icon.iconUrl.startsWith("$circle")) { - // pass - } else { - // @ts-ignore - layer.setIcon(L.icon(icon)) - } - } - } else { - self._geolayer.setStyle(function (featureX) { - return self._style(featureX.properties); - }); - } - } - - let eventSource = State.state.allElements.addOrGetElement(feature); - - - eventSource.addCallback(updateStyle); - - function openPopup(e) { - State.state.selectedElement.setData({feature: feature}); - updateStyle() - if (feature.geometry.type === "Point") { - return; // Points bind there own popups - } - - const uiElement = self._showOnPopup(eventSource, feature); - - L.popup({ - autoPan: true, - }).setContent(uiElement.Render()) - .setLatLng(e.latlng) - .openOn(State.state.bm.map); - uiElement.Update(); - if (e) { - L.DomEvent.stop(e); // Marks the event as consumed - } - } - - layer.on("click", openPopup); - } - }); - - if (this.combinedIsDisplayed.data) { - this._geolayer.addTo(State.state.bm.map); - for (const f of runWhenAdded) { - f(); - } - } + return fusedFeatures; } - - } \ No newline at end of file diff --git a/Logic/Leaflet/GeoLocationHandler.ts b/Logic/Leaflet/GeoLocationHandler.ts index 5975529..e94b23e 100644 --- a/Logic/Leaflet/GeoLocationHandler.ts +++ b/Logic/Leaflet/GeoLocationHandler.ts @@ -4,7 +4,6 @@ import {UIElement} from "../../UI/UIElement"; import State from "../../State"; import {Utils} from "../../Utils"; import {Basemap} from "./Basemap"; -import {FixedUiElement} from "../../UI/Base/FixedUiElement"; import Svg from "../../Svg"; import {Img} from "../../UI/Img"; @@ -48,15 +47,18 @@ export class GeoLocationHandler extends UIElement { map.on('accuratepositionfound', onAccuratePositionFound); map.on('accuratepositionerror', onAccuratePositionError); -FixedUiElement - const icon = L.icon( - { - iconUrl: Img.AsData(Svg.crosshair_blue), - iconSize: [40, 40], // size of the icon - iconAnchor: [20, 20], // point of the icon which will correspond to marker's location - }) + State.state.currentGPSLocation.addCallback((location) => { + + const color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") + const icon = L.icon( + { + iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)), + iconSize: [40, 40], // size of the icon + iconAnchor: [20, 20], // point of the icon which will correspond to marker's location + }) + const newMarker = L.marker(location.latlng, {icon: icon}); newMarker.addTo(map); diff --git a/Logic/MetaTagging.ts b/Logic/MetaTagging.ts index e4acd42..d2991ea 100644 --- a/Logic/MetaTagging.ts +++ b/Logic/MetaTagging.ts @@ -82,7 +82,7 @@ export default class MetaTagging { }) ) private static isOpen = new SimpleMetaTagger( - ["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no", + ["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no')", (feature => { const tagsSource = State.state.allElements.addOrGetElement(feature); tagsSource.addCallback(tags => { @@ -123,16 +123,40 @@ export default class MetaTagging { }) ) - public static carriageWayWidth = new SimpleMetaTagger( - ["_width:needed","_width:needed:no_pedestrians", "_width:difference"], + private static directionSimplified = new SimpleMetaTagger( + ["_direction:simplified", "_direction:leftright"], "_direction:simplified turns 'camera:direction' and 'direction' into either 0, 45, 90, 135, 180, 225, 270 or 315, whichever is closest. _direction:leftright is either 'left' or 'right', which is left-looking on the map or 'right-looking' on the map", + (feature => { + const tags = feature.properties; + const direction = tags["camera:direction"] ?? tags["direction"]; + if (direction === undefined) { + return; + } + let n = Number(direction); + if (isNaN(n)) { + return; + } + + // [22.5 -> 67.5] is sector 1 + // [67.5 -> ] is sector 1 + n = (n + 22.5) % 360; + n = Math.floor(n / 45); + tags["_direction:simplified"] = n; + tags["_direction:leftright"] = n <= 3 ? "right" : "left"; + + + }) + ) + + private static carriageWayWidth = new SimpleMetaTagger( + ["_width:needed", "_width:needed:no_pedestrians", "_width:difference"], "Legacy for a specific project calculating the needed width for safe traffic on a road. Only activated if 'width:carriageway' is present", (feature: any, index: number) => { const properties = feature.properties; - if(properties["width:carriageway"] === undefined){ + if (properties["width:carriageway"] === undefined) { return; } - + const carWidth = 2; const cyclistWidth = 1.5; const pedestrianWidth = 0.75; @@ -239,7 +263,8 @@ export default class MetaTagging { MetaTagging.surfaceArea, MetaTagging.country, MetaTagging.isOpen, - MetaTagging.carriageWayWidth + MetaTagging.carriageWayWidth, + MetaTagging.directionSimplified ]; diff --git a/Logic/UpdateFromOverpass.ts b/Logic/UpdateFromOverpass.ts index 8cd6bbe..d5fe1a2 100644 --- a/Logic/UpdateFromOverpass.ts +++ b/Logic/UpdateFromOverpass.ts @@ -62,6 +62,11 @@ export class UpdateFromOverpass { if (state.locationControl.data.zoom < layer.minzoom) { continue; } + if(layer.doNotDownload){ + continue; + } + + // Check if data for this layer has already been loaded let previouslyLoaded = false; for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) { @@ -128,6 +133,7 @@ export class UpdateFromOverpass { this.ForceRefresh(); console.log(`QUERY FAILED (retrying in ${5 * this.retries.data} sec)`, reason); this.retries.ping(); + this.runningQuery.setData(false) const self = this; window?.setTimeout( function () { diff --git a/Logic/Web/Hash.ts b/Logic/Web/Hash.ts new file mode 100644 index 0000000..6ca2d99 --- /dev/null +++ b/Logic/Web/Hash.ts @@ -0,0 +1,18 @@ +import {UIEventSource} from "../UIEventSource"; + +export default class Hash { + + public static Get() : UIEventSource{ + const hash = new UIEventSource(window.location.hash.substr(1)); + hash.addCallback(h => { + h = h.replace(/\//g, "_"); + return window.location.hash = "#" + h; + }); + window.onhashchange = () => { + hash.setData(window.location.hash.substr(1)) + } + + return hash; + } + +} \ No newline at end of file diff --git a/Logic/Web/Imgur.ts b/Logic/Web/Imgur.ts index ec47fc3..6fe620c 100644 --- a/Logic/Web/Imgur.ts +++ b/Logic/Web/Imgur.ts @@ -56,8 +56,8 @@ export class Imgur { }, }; $.ajax(settings).done(function (response) { - const descr : string= response.data.description; - const data : any = {}; + const descr: string = response.data.description ?? ""; + const data: any = {}; for (const tag of descr.split("\n")) { const kv = tag.split(":"); const k = kv[0]; diff --git a/Logic/Web/QueryParameters.ts b/Logic/Web/QueryParameters.ts index 21a57cc..dd8456e 100644 --- a/Logic/Web/QueryParameters.ts +++ b/Logic/Web/QueryParameters.ts @@ -2,23 +2,26 @@ * Wraps the query parameters into UIEventSources */ import {UIEventSource} from "../UIEventSource"; +import Hash from "./Hash"; export class QueryParameters { - private static order: string [] = ["layout","test","z","lat","lon"]; + private static order: string [] = ["layout", "test", "z", "lat", "lon"]; private static knownSources = {}; private static initialized = false; private static defaults = {} - - private static addOrder(key){ - if(this.order.indexOf(key) < 0){ + + private static documentation = {} + + private static addOrder(key) { + if (this.order.indexOf(key) < 0) { this.order.push(key) } } private static init() { - - if(this.initialized){ + + if (this.initialized) { return; } this.initialized = true; @@ -55,14 +58,15 @@ export class QueryParameters { parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data)) } - history.replaceState(null, "", "?" + parts.join("&")); + history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.Get().data); } - public static GetQueryParameter(key: string, deflt: string): UIEventSource { + public static GetQueryParameter(key: string, deflt: string, documentation?: string): UIEventSource { if(!this.initialized){ this.init(); } + QueryParameters.documentation[key] = documentation; if (deflt !== undefined) { QueryParameters.defaults[key] = deflt; } @@ -76,4 +80,12 @@ export class QueryParameters { return source; } + public static GenerateQueryParameterDocs(): string { + const docs = []; + for (const key in QueryParameters.documentation) { + docs.push("**" + key + "**: " + QueryParameters.documentation[key] + " (default value: _" + QueryParameters.defaults[key] + "_)") + } + return docs.join("\n\n"); + } + } \ No newline at end of file diff --git a/README.md b/README.md index 797ba4a..f5393a6 100644 --- a/README.md +++ b/README.md @@ -103,8 +103,8 @@ A theme has translations into the preset.json (`assets/themes/themename/themenam 0. Fork this repository 1. Modify `"language"` to contain the new language, e.g. `"language": "nl"` becomes `"language": ["nl", "en"]` -2. Add extra strings to the texts. If it used to be a single-language theme, one can replace the strings, e.g.: `"description": "Welcome to Open Bookcase Map"` to `"description": {"en": "Welcome to Open Bookcase Map", "nl": "Welkom bij de OpenBoekenruilkastenKaart", "fr": "Bienvenue sûr la carte des microbibliotheques"}`. If the correct language is not found, it'll fallback to another supported language. -3. If you notice missing translations in the core of MapComplete, fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/UI/i18n/Translations.ts), add add a language string there +2. Add extra strings to the texts. If it used to be a single-language theme, one can replace the strings, e.g.: `"description": "Welcome to Open Bookcase Map"` to `"description": {"en": "Welcome to Open Bookcase Map", "nl": "Welkom bij de OpenBoekenruilkastenKaart", "fr": "Bienvenue sûr la carte des petites bibliotheques"}`. If the correct language is not found, it'll fallback to another supported language. +3. If you notice missing translations in the core of MapComplete, fork this project, open [the file containing all translations](https://github.com/pietervdvn/MapComplete/blob/master/assets/translations.json), add add a language string there 4. Send a pull request to update the languages, I'll gladly add it! It doesn't have to be a complete translation from the start ;) ### Adding your theme to the repository @@ -166,6 +166,50 @@ Whenever a change is made -even adding a single tag- the change is uploaded into Note that changesets are closed automatically after one hour of inactivity, so we don't have to worry about closing them. +### Query parameters + +By adding extra query parameters, more options are available to influence: + +**test**: If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org (default value: _false_) + +**layout**: The layout to load into MapComplete (default value: _bookcases_) + +**userlayout**: undefined (default value: _false_) + +**layer-control-toggle**: Wether or not the layer control is shown (default value: _false_) + +**tab**: The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >200 changesets) (default value: _0_) + +**z**: The initial/current zoom level (default value: _1_) + +**lat**: The initial/current latitude (default value: _0_) + +**lon**: The initial/current longitude of the app (default value: _0_) + +**fs-userbadge**: Disables/Enables the userbadge (and thus disables login capabilities) (default value: _true_) + +**fs-search**: Disables/Enables the search bar (default value: _true_) + +**fs-layers**: Disables/Enables the layer control (default value: _true_) + +**fs-add-new**: Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place) (default value: _true_) + +**fs-welcome-message**: undefined (default value: _true_) + +**fs-iframe**: Disables/Enables the iframe-popup (default value: _false_) + +**fs-more-quests**: Disables/Enables the 'More Quests'-tab in the welcome message (default value: _true_) + +**fs-share-screen**: Disables/Enables the 'Share-screen'-tab in the welcome message (default value: _true_) + +**fs-geolocation**: Disables/Enables the geolocation button (default value: _true_) + +**oauth_token**: Used to complete the login (default value: _undefined_) + +**background**: The id of the background layer to start with (default value: _undefined_) + +**layer-bookcases**: Wehter or not layer bookcases is shown (default value: _true_) index.ts:104:8 + # Privacy Privacy is important, we try to leak as little information as possible. diff --git a/State.ts b/State.ts index d739d42..ec61047 100644 --- a/State.ts +++ b/State.ts @@ -12,6 +12,7 @@ import {LocalStorageSource} from "./Logic/Web/LocalStorageSource"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {BaseLayer} from "./Logic/BaseLayer"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import Hash from "./Logic/Web/Hash"; /** * Contains the global state: a bunch of UI-event sources @@ -21,9 +22,9 @@ export default class State { // The singleton of the global state public static state: State; - - public static vNumber = "0.1.2g"; - + + public static vNumber = "0.2.0"; + // The user journey states thresholds when a new feature gets unlocked public static userJourney = { addNewPointsUnlock: 0, @@ -82,7 +83,7 @@ export default class State { /** The latest element that was selected - used to generate the right UI at the right place */ - public readonly selectedElement = new UIEventSource<{ feature: any }>(undefined); + public readonly selectedElement = new UIEventSource(undefined); public readonly zoom: UIEventSource; public readonly lat: UIEventSource; @@ -115,10 +116,10 @@ export default class State { public layoutDefinition: string; public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>; - public layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false") + public layerControlIsOpened: UIEventSource = QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Wether or not the layer control is shown") .map((str) => str !== "false", [], b => "" + b) - public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0").map( + public welcomeMessageOpenedTab = QueryParameters.GetQueryParameter("tab", "0", `The tab that is shown in the welcome-message. 0 = the explanation of the theme,1 = OSM-credits, 2 = sharescreen, 3 = more themes, 4 = about mapcomplete (user must be logged in and have >${State.userJourney.mapCompleteHelpUnlock} changesets)`).map( str => isNaN(Number(str)) ? 0 : Number(str), [], n => "" + n ); @@ -138,11 +139,11 @@ export default class State { }) } this.zoom = asFloat( - QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom) + QueryParameters.GetQueryParameter("z", "" + layoutToUse.startZoom, "The initial/current zoom level") .syncWith(LocalStorageSource.Get("zoom"))); - this.lat = asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat) + this.lat = asFloat(QueryParameters.GetQueryParameter("lat", "" + layoutToUse.startLat, "The initial/current latitude") .syncWith(LocalStorageSource.Get("lat"))); - this.lon = asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon) + this.lon = asFloat(QueryParameters.GetQueryParameter("lon", "" + layoutToUse.startLon, "The initial/current longitude of the app") .syncWith(LocalStorageSource.Get("lon"))); @@ -165,39 +166,66 @@ export default class State { }); - function featSw(key: string, deflt: (layout: LayoutConfig) => boolean): UIEventSource { - const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined); + function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource { + const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation); // I'm so sorry about someone trying to decipher this // It takes the current layout, extracts the default value for this query paramter. A query parameter event source is then retreived and flattened return UIEventSource.flatten( self.layoutToUse.map((layout) => { const defaultValue = deflt(layout); - const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue) + const queryParam = QueryParameters.GetQueryParameter(key, "" + defaultValue, documentation) return queryParam.map((str) => str === undefined ? defaultValue : (str !== "false")); }), [queryParameterSource]); } - this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true); - this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true); - this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true); - this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true); - this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true); - this.featureSwitchIframe = featSw("fs-iframe", () => false); - this.featureSwitchMoreQuests = featSw("fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true); - this.featureSwitchShareScreen = featSw("fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true); - this.featureSwitchGeolocation = featSw("fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true); + this.featureSwitchUserbadge = featSw("fs-userbadge", (layoutToUse) => layoutToUse?.enableUserBadge ?? true, + "Disables/Enables the user information pill (userbadge) at the top left. Disabling this disables logging in and thus disables editing all together, effectively putting MapComplete into read-only mode."); + this.featureSwitchSearch = featSw("fs-search", (layoutToUse) => layoutToUse?.enableSearch ?? true, + "Disables/Enables the search bar"); + this.featureSwitchLayers = featSw("fs-layers", (layoutToUse) => layoutToUse?.enableLayers ?? true, + "Disables/Enables the layer control"); + this.featureSwitchAddNew = featSw("fs-add-new", (layoutToUse) => layoutToUse?.enableAddNewPoints ?? true, + "Disables/Enables the 'add new feature'-popup. (A theme without presets might not have it in the first place)"); + this.featureSwitchWelcomeMessage = featSw("fs-welcome-message", () => true, + "Disables/enables the help menu or welcome message"); + this.featureSwitchIframe = featSw("fs-iframe", () => false, + "Disables/Enables the iframe-popup"); + this.featureSwitchMoreQuests = featSw("fs-more-quests", (layoutToUse) => layoutToUse?.enableMoreQuests ?? true, + "Disables/Enables the 'More Quests'-tab in the welcome message"); + this.featureSwitchShareScreen = featSw("fs-share-screen", (layoutToUse) => layoutToUse?.enableShareScreen ?? true, + "Disables/Enables the 'Share-screen'-tab in the welcome message"); + this.featureSwitchGeolocation = featSw("fs-geolocation", (layoutToUse) => layoutToUse?.enableGeolocation ?? true, + "Disables/Enables the geolocation button"); - const testParam = QueryParameters.GetQueryParameter("test", "false").data; + const testParam = QueryParameters.GetQueryParameter("test", "false", + "If true, 'dryrun' mode is activated. The app will behave as normal, except that changes to OSM will be printed onto the console instead of actually uploaded to osm.org").data; this.osmConnection = new OsmConnection( testParam === "true", - QueryParameters.GetQueryParameter("oauth_token", undefined), + QueryParameters.GetQueryParameter("oauth_token", undefined, + "Used to complete the login"), layoutToUse.id, true ); + const h = Hash.Get(); + this.selectedElement.addCallback(selected => { + if (selected === undefined) { + h.setData(""); + } else { + h.setData(selected.id) + } + } + ) + h.addCallbackAndRun(hash => { + if(hash === undefined || hash === ""){ + self.selectedElement.setData(undefined); + } + }) + + this.installedThemes = this.osmConnection.preferencesHandler.preferences.map<{ layout: LayoutConfig, definition: string }[]>(allPreferences => { const installedThemes: { layout: LayoutConfig, definition: string }[] = []; if (allPreferences === undefined) { @@ -213,8 +241,10 @@ export default class State { continue; } try { + const json = btoa(customLayout.data); + console.log(json); const layout = new LayoutConfig( - JSON.parse(btoa(customLayout.data))); + JSON.parse(json)); installedThemes.push({ layout: layout, definition: customLayout.data diff --git a/Svg.ts b/Svg.ts index 7a11237..fbcbcdd 100644 --- a/Svg.ts +++ b/Svg.ts @@ -4,160 +4,219 @@ import {FixedUiElement} from "./UI/Base/FixedUiElement"; export default class Svg { - public static add = " image/svg+xml " + public static add = " image/svg+xml " public static add_img = Img.AsImageElement(Svg.add) + public static add_svg() { return new FixedUiElement(Svg.add);} public static add_ui() { return new FixedUiElement(Svg.add_img);} - public static addSmall = " image/svg+xml " + public static addSmall = " image/svg+xml " public static addSmall_img = Img.AsImageElement(Svg.addSmall) + public static addSmall_svg() { return new FixedUiElement(Svg.addSmall);} public static addSmall_ui() { return new FixedUiElement(Svg.addSmall_img);} - public static ampersand = " image/svg+xml " + public static ampersand = "e image/svg+xml " public static ampersand_img = Img.AsImageElement(Svg.ampersand) + public static ampersand_svg() { return new FixedUiElement(Svg.ampersand);} public static ampersand_ui() { return new FixedUiElement(Svg.ampersand_img);} - public static arrow_left_smooth = " image/svg+xml " + public static arrow_left_smooth = " image/svg+xml " public static arrow_left_smooth_img = Img.AsImageElement(Svg.arrow_left_smooth) + public static arrow_left_smooth_svg() { return new FixedUiElement(Svg.arrow_left_smooth);} public static arrow_left_smooth_ui() { return new FixedUiElement(Svg.arrow_left_smooth_img);} - public static arrow_right_smooth = " image/svg+xml " + public static arrow_right_smooth = " image/svg+xml " public static arrow_right_smooth_img = Img.AsImageElement(Svg.arrow_right_smooth) + public static arrow_right_smooth_svg() { return new FixedUiElement(Svg.arrow_right_smooth);} public static arrow_right_smooth_ui() { return new FixedUiElement(Svg.arrow_right_smooth_img);} public static bug = " " public static bug_img = Img.AsImageElement(Svg.bug) + public static bug_svg() { return new FixedUiElement(Svg.bug);} public static bug_ui() { return new FixedUiElement(Svg.bug_img);} - public static camera_plus = " image/svg+xml " + public static camera_plus = " image/svg+xml " public static camera_plus_img = Img.AsImageElement(Svg.camera_plus) + public static camera_plus_svg() { return new FixedUiElement(Svg.camera_plus);} public static camera_plus_ui() { return new FixedUiElement(Svg.camera_plus_img);} - public static checkmark = "" + public static checkmark = "" public static checkmark_img = Img.AsImageElement(Svg.checkmark) + public static checkmark_svg() { return new FixedUiElement(Svg.checkmark);} public static checkmark_ui() { return new FixedUiElement(Svg.checkmark_img);} - public static close = " image/svg+xml " + public static close = " image/svg+xml " public static close_img = Img.AsImageElement(Svg.close) + public static close_svg() { return new FixedUiElement(Svg.close);} public static close_ui() { return new FixedUiElement(Svg.close_img);} - public static crosshair_blue_center = " image/svg+xml " + public static compass = " image/svg+xml N S E W NW SW NE SE " + public static compass_img = Img.AsImageElement(Svg.compass) + public static compass_svg() { return new FixedUiElement(Svg.compass);} + public static compass_ui() { return new FixedUiElement(Svg.compass_img);} + + public static crosshair_blue_center = " image/svg+xml " public static crosshair_blue_center_img = Img.AsImageElement(Svg.crosshair_blue_center) + public static crosshair_blue_center_svg() { return new FixedUiElement(Svg.crosshair_blue_center);} public static crosshair_blue_center_ui() { return new FixedUiElement(Svg.crosshair_blue_center_img);} - public static crosshair_blue = " image/svg+xml " + public static crosshair_blue = " image/svg+xml " public static crosshair_blue_img = Img.AsImageElement(Svg.crosshair_blue) + public static crosshair_blue_svg() { return new FixedUiElement(Svg.crosshair_blue);} public static crosshair_blue_ui() { return new FixedUiElement(Svg.crosshair_blue_img);} - public static crosshair = " image/svg+xml " + public static crosshair = " image/svg+xml " public static crosshair_img = Img.AsImageElement(Svg.crosshair) + public static crosshair_svg() { return new FixedUiElement(Svg.crosshair);} public static crosshair_ui() { return new FixedUiElement(Svg.crosshair_img);} - public static delete_icon = " image/svg+xml " + public static delete_icon = " image/svg+xml " public static delete_icon_img = Img.AsImageElement(Svg.delete_icon) + public static delete_icon_svg() { return new FixedUiElement(Svg.delete_icon);} public static delete_icon_ui() { return new FixedUiElement(Svg.delete_icon_img);} - public static down = " image/svg+xml " + public static direction = " image/svg+xml " + public static direction_img = Img.AsImageElement(Svg.direction) + public static direction_svg() { return new FixedUiElement(Svg.direction);} + public static direction_ui() { return new FixedUiElement(Svg.direction_img);} + + public static direction_gradient = " image/svg+xml " + public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient) + public static direction_gradient_svg() { return new FixedUiElement(Svg.direction_gradient);} + public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);} + + public static down = " image/svg+xml " public static down_img = Img.AsImageElement(Svg.down) + public static down_svg() { return new FixedUiElement(Svg.down);} public static down_ui() { return new FixedUiElement(Svg.down_img);} - public static envelope = " " + public static envelope = " image/svg+xml " public static envelope_img = Img.AsImageElement(Svg.envelope) + public static envelope_svg() { return new FixedUiElement(Svg.envelope);} public static envelope_ui() { return new FixedUiElement(Svg.envelope_img);} - public static floppy = " " + public static floppy = " " public static floppy_img = Img.AsImageElement(Svg.floppy) + public static floppy_svg() { return new FixedUiElement(Svg.floppy);} public static floppy_ui() { return new FixedUiElement(Svg.floppy_img);} - public static gear = "" + public static gear = " " public static gear_img = Img.AsImageElement(Svg.gear) + public static gear_svg() { return new FixedUiElement(Svg.gear);} public static gear_ui() { return new FixedUiElement(Svg.gear_img);} - public static help = " image/svg+xml " + public static help = " " public static help_img = Img.AsImageElement(Svg.help) + public static help_svg() { return new FixedUiElement(Svg.help);} public static help_ui() { return new FixedUiElement(Svg.help_img);} - public static home = " " + public static home = " " public static home_img = Img.AsImageElement(Svg.home) + public static home_svg() { return new FixedUiElement(Svg.home);} public static home_ui() { return new FixedUiElement(Svg.home_img);} - public static josm_logo = " JOSM Logotype 2019 image/svg+xml JOSM Logotype 2019 2019-08-05 Diamond00744 Public Domain " + public static home_white_bg = " image/svg+xml " + public static home_white_bg_img = Img.AsImageElement(Svg.home_white_bg) + public static home_white_bg_svg() { return new FixedUiElement(Svg.home_white_bg);} + public static home_white_bg_ui() { return new FixedUiElement(Svg.home_white_bg_img);} + + public static josm_logo = " JOSM Logotype 2019 image/svg+xml JOSM Logotype 2019 2019-08-05 Diamond00744 Public Domain " public static josm_logo_img = Img.AsImageElement(Svg.josm_logo) + public static josm_logo_svg() { return new FixedUiElement(Svg.josm_logo);} public static josm_logo_ui() { return new FixedUiElement(Svg.josm_logo_img);} - public static layers = " image/svg+xml " + public static layers = " image/svg+xml " public static layers_img = Img.AsImageElement(Svg.layers) + public static layers_svg() { return new FixedUiElement(Svg.layers);} public static layers_ui() { return new FixedUiElement(Svg.layers_img);} - public static layersAdd = " image/svg+xml " + public static layersAdd = " image/svg+xml " public static layersAdd_img = Img.AsImageElement(Svg.layersAdd) + public static layersAdd_svg() { return new FixedUiElement(Svg.layersAdd);} public static layersAdd_ui() { return new FixedUiElement(Svg.layersAdd_img);} - public static logo = " image/svg+xml " + public static logo = " image/svg+xml " public static logo_img = Img.AsImageElement(Svg.logo) + public static logo_svg() { return new FixedUiElement(Svg.logo);} public static logo_ui() { return new FixedUiElement(Svg.logo_img);} - public static logout = " image/svg+xml " + public static logout = " image/svg+xml " public static logout_img = Img.AsImageElement(Svg.logout) + public static logout_svg() { return new FixedUiElement(Svg.logout);} public static logout_ui() { return new FixedUiElement(Svg.logout_img);} public static mapillary = "" public static mapillary_img = Img.AsImageElement(Svg.mapillary) + public static mapillary_svg() { return new FixedUiElement(Svg.mapillary);} public static mapillary_ui() { return new FixedUiElement(Svg.mapillary_img);} public static no_checkmark = " " public static no_checkmark_img = Img.AsImageElement(Svg.no_checkmark) + public static no_checkmark_svg() { return new FixedUiElement(Svg.no_checkmark);} public static no_checkmark_ui() { return new FixedUiElement(Svg.no_checkmark_img);} - public static or = " image/svg+xml " + public static or = " image/svg+xml " public static or_img = Img.AsImageElement(Svg.or) + public static or_svg() { return new FixedUiElement(Svg.or);} public static or_ui() { return new FixedUiElement(Svg.or_img);} public static osm_logo_us = "" public static osm_logo_us_img = Img.AsImageElement(Svg.osm_logo_us) + public static osm_logo_us_svg() { return new FixedUiElement(Svg.osm_logo_us);} public static osm_logo_us_ui() { return new FixedUiElement(Svg.osm_logo_us_img);} - public static osm_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011 010110010011010110010011 " + public static osm_logo = " OpenStreetMap logo 2011 image/svg+xml OpenStreetMap logo 2011 Ken Vermette April 2011 OpenStreetMap.org Replacement logo for OpenStreetMap Foundation OSM openstreetmap logo http://wiki.openstreetmap.org/wiki/File:Public-images-osm_logo.svg 010110010011010110010011 010110010011010110010011 " public static osm_logo_img = Img.AsImageElement(Svg.osm_logo) + public static osm_logo_svg() { return new FixedUiElement(Svg.osm_logo);} public static osm_logo_ui() { return new FixedUiElement(Svg.osm_logo_img);} - public static pencil = " " + public static pencil = " " public static pencil_img = Img.AsImageElement(Svg.pencil) + public static pencil_svg() { return new FixedUiElement(Svg.pencil);} public static pencil_ui() { return new FixedUiElement(Svg.pencil_img);} - public static pop_out = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " + public static pop_out = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " public static pop_out_img = Img.AsImageElement(Svg.pop_out) + public static pop_out_svg() { return new FixedUiElement(Svg.pop_out);} public static pop_out_ui() { return new FixedUiElement(Svg.pop_out_img);} - public static reload = " " + public static reload = " " public static reload_img = Img.AsImageElement(Svg.reload) + public static reload_svg() { return new FixedUiElement(Svg.reload);} public static reload_ui() { return new FixedUiElement(Svg.reload_img);} - public static search = " " + public static search = " " public static search_img = Img.AsImageElement(Svg.search) + public static search_svg() { return new FixedUiElement(Svg.search);} public static search_ui() { return new FixedUiElement(Svg.search_img);} - public static share = " image/svg+xml " + public static share = " image/svg+xml " public static share_img = Img.AsImageElement(Svg.share) + public static share_svg() { return new FixedUiElement(Svg.share);} public static share_ui() { return new FixedUiElement(Svg.share_img);} - public static star = " Created by potrace 1.15, written by Peter Selinger 2001-2017 " + public static star = " Created by potrace 1.15, written by Peter Selinger 2001-2017 " public static star_img = Img.AsImageElement(Svg.star) + public static star_svg() { return new FixedUiElement(Svg.star);} public static star_ui() { return new FixedUiElement(Svg.star_img);} - public static statistics = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " + public static statistics = " Svg Vector Icons : http://www.onlinewebfonts.com/icon " public static statistics_img = Img.AsImageElement(Svg.statistics) + public static statistics_svg() { return new FixedUiElement(Svg.statistics);} public static statistics_ui() { return new FixedUiElement(Svg.statistics_img);} - public static up = " " + public static up = " " public static up_img = Img.AsImageElement(Svg.up) + public static up_svg() { return new FixedUiElement(Svg.up);} public static up_ui() { return new FixedUiElement(Svg.up_img);} - public static wikimedia_commons_white = " Wikimedia Commons Logo " + public static wikimedia_commons_white = " Wikimedia Commons Logo " public static wikimedia_commons_white_img = Img.AsImageElement(Svg.wikimedia_commons_white) + public static wikimedia_commons_white_svg() { return new FixedUiElement(Svg.wikimedia_commons_white);} public static wikimedia_commons_white_ui() { return new FixedUiElement(Svg.wikimedia_commons_white_img);} - public static wikipedia = " Wikipedia logo version 2" + public static wikipedia = " Wikipedia logo version 2" public static wikipedia_img = Img.AsImageElement(Svg.wikipedia) + public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);} public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);} -} +public static All = {"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,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"close.svg": Svg.close,"compass.svg": Svg.compass,"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,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"search.svg": Svg.search,"share.svg": Svg.share,"star.svg": Svg.star,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};} diff --git a/UI/FullScreenMessageBoxHandler.ts b/UI/FullScreenMessageBoxHandler.ts index bf12c48..70d5d90 100644 --- a/UI/FullScreenMessageBoxHandler.ts +++ b/UI/FullScreenMessageBoxHandler.ts @@ -8,64 +8,23 @@ import Combine from "./Base/Combine"; */ export class FullScreenMessageBox extends UIElement { - private static readonly _toTheMap_height : string = "5em"; - - private _uielement: UIElement; private readonly returnToTheMap: UIElement; + private _content: UIElement; constructor(onClear: (() => void)) { super(State.state.fullScreenMessage); - const self = this; - State.state.fullScreenMessage.addCallbackAndRun(uiElement => { - this._uielement = new Combine([State.state.fullScreenMessage.data]).SetStyle( - "display:block;"+ - "padding: 1em;"+ - "padding-bottom:6em;"+ - `margin-bottom:${FullScreenMessageBox._toTheMap_height};`+ - "box-sizing:border-box;"+ - `height:calc(100vh - ${FullScreenMessageBox._toTheMap_height});`+ - "overflow-y: auto;" + - "max-width:100vw;" + - "overflow-x:hidden;" + - "background:white;" - - ); - }); - - this.HideOnEmpty(true); - State.state.fullScreenMessage.addCallback(latestData => { - if (latestData === undefined) { - location.hash = ""; - } else { - // The 'hash' makes sure a new piece of history is added. This makes the 'back-button' on android remove the popup - location.hash = "#element"; - } - this.Update(); - }) - - if (window !== undefined) { - window.onhashchange = function () { - if (location.hash === "") { - // No more element: back to the map! - console.log("Clearing full screen message"); - State.state.fullScreenMessage.setData(undefined); - onClear(); - } - } - } - this.returnToTheMap = new Combine([Translations.t.general.returnToTheMap.Clone().SetStyle("font-size:xx-large")]) - .SetStyle("background:#7ebc6f;" + - "position: fixed;" + - "z-index: 10000;" + - "bottom: 0;" + - "left: 0;" + - `height: ${FullScreenMessageBox._toTheMap_height};` + + .SetStyle("background:var(--catch-detail-color);" + + "position: fixed;" + + "z-index: 10000;" + + "bottom: 0;" + + "left: 0;" + + `height: var(--return-to-the-map-height);` + "width: 100vw;" + - "color: white;" + + "color: var(--catch-detail-color-contrast);" + "font-weight: bold;" + "pointer-events: all;" + "cursor: pointer;" + @@ -74,10 +33,8 @@ export class FullScreenMessageBox extends UIElement { "padding-bottom: 1.2em;" + "box-sizing:border-box") .onClick(() => { - console.log("Returning...") State.state.fullScreenMessage.setData(undefined); onClear(); - self.Update(); }); } @@ -87,7 +44,21 @@ export class FullScreenMessageBox extends UIElement { if (State.state.fullScreenMessage.data === undefined) { return ""; } - return new Combine([this._uielement, this.returnToTheMap]) + this._content = State.state.fullScreenMessage.data; + const uielement = new Combine([this._content]).SetStyle( + "display:block;" + + "padding: 1em;" + + "padding-bottom:6em;" + + `margin-bottom: var(--return-to-the-map-height);` + + "box-sizing:border-box;" + + `height:calc(100vh - var(--return-to-the-map-height));` + + "overflow-y: auto;" + + "max-width:100vw;" + + "overflow-x:hidden;" + + "background:var(--background-color);" + + "color: var(--foreground-color);" + ); + return new Combine([uielement, this.returnToTheMap]) .Render(); } diff --git a/UI/Image/DeleteImage.ts b/UI/Image/DeleteImage.ts index b59af06..7e5a2c1 100644 --- a/UI/Image/DeleteImage.ts +++ b/UI/Image/DeleteImage.ts @@ -42,6 +42,9 @@ export default class DeleteImage extends UIElement { } InnerRender(): string { + if(!State.state.featureSwitchUserbadge.data){ + return ""; + } const value = this.tags.data[this.key]; if (value === undefined || value === "") { diff --git a/UI/Image/ImageUploadFlow.ts b/UI/Image/ImageUploadFlow.ts index 0e8815a..21fed61 100644 --- a/UI/Image/ImageUploadFlow.ts +++ b/UI/Image/ImageUploadFlow.ts @@ -44,13 +44,17 @@ export class ImageUploadFlow extends UIElement { this._licensePicker = licensePicker; this._selectedLicence = licensePicker.GetValue(); - this._connectButton = new Combine([t.pleaseLogin]) + this._connectButton = t.pleaseLogin.Clone() .onClick(() => State.state.osmConnection.AttemptLogin()) .SetClass("login-button-friendly"); } InnerRender(): string { + + if(!State.state.featureSwitchUserbadge.data){ + return ""; + } const t = Translations.t.image; if (State.state.osmConnection.userDetails === undefined) { @@ -97,23 +101,10 @@ export class ImageUploadFlow extends UIElement { ]); const label = new Combine([ - Svg.camera_plus_ui().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"), + Svg.camera_plus_svg().SetStyle("width: 36px;height: 36px;padding: 0.1em;margin-top: 5px;border-radius: 0;float: left;display:block"), Translations.t.image.addPicture - .SetStyle("width:max-content;font-size: 28px;" + - "font-weight: bold;" + - "float: left;" + - "margin-top: 4px;" + - "padding-top: 4px;" + - "padding-bottom: 4px;" + - "padding-left: 13px;"), - - ]).SetStyle(" display: flex;" + - "cursor:pointer;" + - "padding: 0.5em;" + - "border-radius: 1em;" + - "border: 3px solid black;" + - "box-sizing:border-box;") - + ]).SetClass("image-upload-flow-button") + const actualInputElement = ``; @@ -127,7 +118,8 @@ export class ImageUploadFlow extends UIElement { return new Combine([ form, extraInfo - ]).SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;") + ]).SetClass("image-upload-flow") + .SetStyle("margin-top: 1em;margin-bottom: 2em;text-align: center;") .Render(); } diff --git a/UI/Input/DirectionInput.ts b/UI/Input/DirectionInput.ts new file mode 100644 index 0000000..3fe8344 --- /dev/null +++ b/UI/Input/DirectionInput.ts @@ -0,0 +1,100 @@ +import {InputElement} from "./InputElement"; +import {UIEventSource} from "../../Logic/UIEventSource"; +import Combine from "../Base/Combine"; +import Svg from "../../Svg"; +import * as L from "leaflet" +import * as X from "leaflet-providers" +import {Basemap} from "../../Logic/Leaflet/Basemap"; +import State from "../../State"; + +/** + * Selects a direction in degrees + */ +export default class DirectionInput extends InputElement { + + private readonly value: UIEventSource; + public readonly IsSelected: UIEventSource = new UIEventSource(false); + + constructor(value?: UIEventSource) { + super(); + this.dumbMode = false; + this.value = value ?? new UIEventSource(undefined); + + this.value.addCallbackAndRun(rotation => { + const selfElement = document.getElementById(this.id); + if (selfElement === null) { + return; + } + const cone = selfElement.getElementsByClassName("direction-svg")[0] as HTMLElement + cone.style.rotate = rotation + "deg"; + + }) + + } + + + GetValue(): UIEventSource { + return this.value; + } + + InnerRender(): string { + return new Combine([ + `
`, + Svg.direction_svg().SetStyle( + `position: absolute;top: 0;left: 0;width: 100%;height: 100%;rotate:${this.value.data}deg;`) + .SetClass("direction-svg"), + Svg.compass_svg().SetStyle( + "position: absolute;top: 0;left: 0;width: 100%;height: 100%;") + ]) + .SetStyle("position:relative;display:block;width: min(100%, 25em); padding-top: min(100% , 25em); background:white; border: 1px solid black; border-radius: 999em") + .Render(); + } + + protected InnerUpdate(htmlElement: HTMLElement) { + super.InnerUpdate(htmlElement); + const self = this; + + function onPosChange(x: number, y: number) { + const rect = htmlElement.getBoundingClientRect(); + const dx = -(rect.left + rect.right) / 2 + x; + const dy = (rect.top + rect.bottom) / 2 - y; + const angle = 180 * Math.atan2(dy, dx) / Math.PI; + const angleGeo = Math.floor((450 - angle) % 360); + self.value.setData("" + angleGeo) + } + + + htmlElement.ontouchmove = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + ev.preventDefault(); + } + + htmlElement.ontouchstart = (ev: TouchEvent) => { + onPosChange(ev.touches[0].clientX, ev.touches[0].clientY); + } + + let isDown = false; + + htmlElement.onmousedown = (ev: MouseEvent) => { + isDown = true; + onPosChange(ev.x, ev.y); + ev.preventDefault(); + } + + htmlElement.onmouseup = (ev) => { + isDown = false; ev.preventDefault(); + } + + htmlElement.onmousemove = (ev: MouseEvent) => { + if (isDown) { + onPosChange(ev.x, ev.y); + } ev.preventDefault(); + } + } + + IsValid(str: string): boolean { + const t = Number(str); + return !isNaN(t) && t >= 0 && t <= 360; + } + +} \ No newline at end of file diff --git a/UI/Input/MultiInput.ts b/UI/Input/MultiInput.ts index 22f9fdf..3e40480 100644 --- a/UI/Input/MultiInput.ts +++ b/UI/Input/MultiInput.ts @@ -71,7 +71,7 @@ export class MultiInput extends InputElement { input.IsSelected.addCallback(() => this.UpdateIsSelected()); const moveUpBtn = Svg.up_ui() - .onClick(() => { + .SetClass('small-image').onClick(() => { const v = self._value.data[i]; self._value.data[i] = self._value.data[i - 1]; self._value.data[i - 1] = v; @@ -79,8 +79,8 @@ export class MultiInput extends InputElement { }); const moveDownBtn = - Svg.down_ui().SetStyle('max-width: 1.5em; margin-left: 5px;display:block;') - .onClick(() => { + Svg.down_ui() + .SetClass('small-image') .onClick(() => { const v = self._value.data[i]; self._value.data[i] = self._value.data[i + 1]; self._value.data[i + 1] = v; @@ -98,7 +98,7 @@ export class MultiInput extends InputElement { const deleteBtn = - Svg.delete_icon_ui().SetStyle('max-width: 1.5em;width:1.5em; margin-left: 5px;') + Svg.delete_icon_ui().SetClass('small-image') .onClick(() => { self._value.data.splice(i, 1); self._value.ping(); diff --git a/UI/Input/ValidatedTextField.ts b/UI/Input/ValidatedTextField.ts index 2c89ef6..703ed19 100644 --- a/UI/Input/ValidatedTextField.ts +++ b/UI/Input/ValidatedTextField.ts @@ -9,13 +9,16 @@ import {UIEventSource} from "../../Logic/UIEventSource"; import CombinedInputElement from "./CombinedInputElement"; import SimpleDatePicker from "./SimpleDatePicker"; import OpeningHoursInput from "./OpeningHours/OpeningHoursInput"; +import DirectionInput from "./DirectionInput"; interface TextFieldDef { name: string, explanation: string, isValid: ((s: string, country?: string) => boolean), reformat?: ((s: string, country?: string) => string), - inputHelper?: (value: UIEventSource) => InputElement, + inputHelper?: (value: UIEventSource, options?: { + location: [number, number] + }) => InputElement, } export default class ValidatedTextField { @@ -25,7 +28,9 @@ export default class ValidatedTextField { explanation: string, isValid?: ((s: string, country?: string) => boolean), reformat?: ((s: string, country?: string) => string), - inputHelper?: (value: UIEventSource) => InputElement): TextFieldDef { + inputHelper?: (value: UIEventSource, options?:{ + location: [number, number] + }) => InputElement): TextFieldDef { if (isValid === undefined) { isValid = () => true; @@ -97,6 +102,17 @@ export default class ValidatedTextField { str = "" + str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0 }), + ValidatedTextField.tp( + "direction", + "A geographical direction, in degrees. 0° is north, 90° is east, ... Will return a value between 0 (incl) and 360 (excl)", + (str) => { + str = "" + str; + return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) >= 0 && Number(str) <= 360 + },str => str, + (value) => { + return new DirectionInput(value); + } + ), ValidatedTextField.tp( "float", "A decimal", @@ -185,7 +201,8 @@ export default class ValidatedTextField { textArea?: boolean, textAreaRows?: number, isValid?: ((s: string, country: string) => boolean), - country?: string + country?: string, + location?: [number /*lat*/, number /*lon*/] }): InputElement { options = options ?? {}; options.placeholder = options.placeholder ?? type; @@ -218,7 +235,9 @@ export default class ValidatedTextField { } if (tp.inputHelper) { - input = new CombinedInputElement(input, tp.inputHelper(input.GetValue())); + input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(),{ + location: options.location + })); } return input; } diff --git a/UI/Popup/EditableTagRendering.ts b/UI/Popup/EditableTagRendering.ts index 931b570..ea61c89 100644 --- a/UI/Popup/EditableTagRendering.ts +++ b/UI/Popup/EditableTagRendering.ts @@ -27,24 +27,27 @@ export default class EditableTagRendering extends UIElement { this.ListenTo(this._editMode); this.ListenTo(State.state?.osmConnection?.userDetails) - const self = this; - this._answer = new TagRenderingAnswer(tags, configuration); - this._answer.SetStyle("width:100%;") + this._question = this.GenerateQuestion(); + this.dumbMode = false; if (this._configuration.question !== undefined) { - // 2.3em total width - if(State.state.featureSwitchUserbadge.data){ - - this._editButton = - Svg.pencil_ui().SetClass("edit-button") - .onClick(() => { - self._editMode.setData(true); - }); + if (State.state.featureSwitchUserbadge.data) { + // 2.3em total width + const self = this; + this._editButton = + Svg.pencil_svg().SetClass("edit-button") + .onClick(() => { + self._editMode.setData(true); + }); } + } + } - + private GenerateQuestion() { + const self = this; + if (this._configuration.question !== undefined) { // And at last, set up the skip button const cancelbutton = Translations.t.general.cancel.Clone() @@ -53,7 +56,7 @@ export default class EditableTagRendering extends UIElement { self._editMode.setData(false) }); - this._question = new TagRenderingQuestion(tags, configuration, + return new TagRenderingQuestion(this._tags, this._configuration, () => { self._editMode.setData(false) }, diff --git a/UI/Popup/FeatureInfoBox.ts b/UI/Popup/FeatureInfoBox.ts index abc4b6c..9a669dc 100644 --- a/UI/Popup/FeatureInfoBox.ts +++ b/UI/Popup/FeatureInfoBox.ts @@ -21,15 +21,16 @@ export class FeatureInfoBox extends UIElement { layerConfig: LayerConfig ) { super(); - if(layerConfig === undefined){ + if (layerConfig === undefined) { throw "Undefined layerconfig" } this._tags = tags; this._layerConfig = layerConfig; - this._title = new TagRenderingAnswer(tags, layerConfig.title) - .SetClass("featureinfobox-title"); + this._title = layerConfig.title === undefined ? undefined : + new TagRenderingAnswer(tags, layerConfig.title) + .SetClass("featureinfobox-title"); this._titleIcons = new Combine( layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon))) .SetClass("featureinfobox-icons"); @@ -37,7 +38,6 @@ export class FeatureInfoBox extends UIElement { if (State.state.featureSwitchUserbadge.data) { this._questionBox = new QuestionBox(tags, layerConfig.tagRenderings); } - } InnerRender(): string { diff --git a/UI/Popup/TagRenderingAnswer.ts b/UI/Popup/TagRenderingAnswer.ts index 8c9f6dd..d5e062d 100644 --- a/UI/Popup/TagRenderingAnswer.ts +++ b/UI/Popup/TagRenderingAnswer.ts @@ -15,19 +15,9 @@ export default class TagRenderingAnswer extends UIElement { super(tags); this._tags = tags; this._configuration = configuration; - const self = this; - tags.addCallbackAndRun(tags => { - if (tags === undefined) { - self._content = undefined - return; - } - const tr = this._configuration.GetRenderValue(tags); - if (tr === undefined) { - self._content = undefined - return - } - self._content = new SubstitutedTranslation(tr, self._tags) - }) + if(configuration === undefined){ + throw "Trying to generate a tagRenderingAnswer without configuration..." + } } InnerRender(): string { @@ -36,9 +26,17 @@ export default class TagRenderingAnswer extends UIElement { return ""; } } - if(this._content === undefined){ + + const tags = this._tags.data; + if (tags === undefined) { return ""; } + const tr = this._configuration.GetRenderValue(tags); + if (tr === undefined) { + return ""; + } + // Bit of a hack; remember that the fields are updated + this._content = new SubstitutedTranslation(tr, this._tags); return this._content.Render(); } diff --git a/UI/Popup/TagRenderingQuestion.ts b/UI/Popup/TagRenderingQuestion.ts index 10ba6bc..68f48f9 100644 --- a/UI/Popup/TagRenderingQuestion.ts +++ b/UI/Popup/TagRenderingQuestion.ts @@ -118,7 +118,6 @@ export default class TagRenderingQuestion extends UIElement { const inputEl = new InputElementMap( checkBoxes, (t0, t1) => { - console.log("IsEquiv?",t0, t1, t0?.isEquivalent(t1)) return t0?.isEquivalent(t1) ?? false }, (indices) => { @@ -162,8 +161,6 @@ export default class TagRenderingQuestion extends UIElement { } } - console.log(indices, freeformExtras); - if (freeformField) { if (freeformExtras.length > 0) { freeformField.GetValue().setData(new Tag(this._configuration.freeform.key, freeformExtras.join(";"))); @@ -254,7 +251,8 @@ export default class TagRenderingQuestion extends UIElement { const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, { isValid: (str) => (str.length <= 255), - country: this._tags.data._country + country: this._tags.data._country, + location: [this._tags.data._lat, this._tags.data._lon] }); textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]); diff --git a/UI/ShareScreen.ts b/UI/ShareScreen.ts index a682c6c..c9ea026 100644 --- a/UI/ShareScreen.ts +++ b/UI/ShareScreen.ts @@ -32,9 +32,17 @@ export class ShareScreen extends UIElement { const optionCheckboxes: UIElement[] = [] const optionParts: (UIEventSource)[] = []; + function check() { + return Svg.checkmark_svg().SetStyle("width: 1.5em; display:inline-block;"); + } + + function nocheck() { + return Svg.no_checkmark_svg().SetStyle("width: 1.5em; display: inline-block;"); + } + const includeLocation = new CheckBox( - new Combine([Svg.checkmark, tr.fsIncludeCurrentLocation]), - new Combine([Svg.no_checkmark, tr.fsIncludeCurrentLocation]), + new Combine([check(), tr.fsIncludeCurrentLocation]), + new Combine([nocheck(), tr.fsIncludeCurrentLocation]), true ) optionCheckboxes.push(includeLocation); @@ -68,8 +76,8 @@ export class ShareScreen extends UIElement { return tr.fsIncludeCurrentBackgroundMap.Subs({name: layer?.name ?? ""}).Render(); })); const includeCurrentBackground = new CheckBox( - new Combine([Svg.checkmark, currentBackground]), - new Combine([Svg.no_checkmark, currentBackground]), + new Combine([check(), currentBackground]), + new Combine([nocheck(), currentBackground]), true ) optionCheckboxes.push(includeCurrentBackground); @@ -83,8 +91,8 @@ export class ShareScreen extends UIElement { const includeLayerChoices = new CheckBox( - new Combine([Svg.checkmark, tr.fsIncludeCurrentLayers]), - new Combine([Svg.no_checkmark, tr.fsIncludeCurrentLayers]), + new Combine([check(), tr.fsIncludeCurrentLayers]), + new Combine([nocheck(), tr.fsIncludeCurrentLayers]), true ) optionCheckboxes.push(includeLayerChoices); @@ -113,8 +121,8 @@ export class ShareScreen extends UIElement { for (const swtch of switches) { const checkbox = new CheckBox( - new Combine([Svg.checkmark, Translations.W(swtch.human)]), - new Combine([Svg.no_checkmark, Translations.W(swtch.human)]), !swtch.reverse + new Combine([check(), Translations.W(swtch.human)]), + new Combine([nocheck(), Translations.W(swtch.human)]), !swtch.reverse ); optionCheckboxes.push(checkbox); optionParts.push(checkbox.isEnabled.map((isEn) => { diff --git a/UI/SimpleAddUI.ts b/UI/SimpleAddUI.ts index bed3179..db1c600 100644 --- a/UI/SimpleAddUI.ts +++ b/UI/SimpleAddUI.ts @@ -8,7 +8,6 @@ import Locale from "./i18n/Locale"; import State from "../State"; import {UIEventSource} from "../Logic/UIEventSource"; -import {Img} from "./Img"; import Svg from "../Svg"; /** @@ -115,8 +114,8 @@ export class SimpleAddUI extends UIElement { const loc = State.state.bm.LastClickLocation.data; let feature = State.state.changes.createElement(tags, loc.lat, loc.lon); + State.state.selectedElement.setData(feature); layerToAddTo.AddNewElement(feature); - State.state.selectedElement.setData({feature: feature}); } } diff --git a/UI/SpecialVisualizations.ts b/UI/SpecialVisualizations.ts index 75f260f..a20a1bf 100644 --- a/UI/SpecialVisualizations.ts +++ b/UI/SpecialVisualizations.ts @@ -39,12 +39,16 @@ export class SubstitutedTranslation extends UIElement { return [] } const tags = this.tags.data; + txt = SubstitutedTranslation.SubstituteKeys(txt, tags); + return this.EvaluateSpecialComponents(txt); + } + + public static SubstituteKeys(txt: string, tags: any) { for (const key in tags) { // Poor mans replace all txt = txt.split("{" + key + "}").join(tags[key]); } - - return this.EvaluateSpecialComponents(txt); + return txt; } private EvaluateSpecialComponents(template: string): UIElement[] { @@ -98,7 +102,20 @@ export default class SpecialVisualizations { args: { name: string, defaultValue?: string, doc: string }[] }[] = - [ + [{ + funcName: "all_tags", + docs: "Prints all key-value pairs of the object - used for debugging", + args: [], + constr: ((tags: UIEventSource) => { + return new VariableUiElement(tags.map(tags => { + const parts = []; + for (const key in tags) { + parts.push(key + "=" + tags[key]); + } + return parts.join("
") + })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") + }) + }, { funcName: "image_carousel", docs: "Creates an image carousel for the given sources. An attempt will be made to guess what source is used. Supported: Wikidata identifiers, Wikipedia pages, Wikimedia categories, IMGUR (with attribution, direct links)", @@ -166,21 +183,7 @@ export default class SpecialVisualizations { return new VariableUiElement(source.map(data => data[neededValue] ?? "Loading...")); } }, - - { - funcName: "all_tags", - docs: "Prints all key-value pairs of the object - used for debugging", - args:[], - constr: ((tags: UIEventSource) => { - return new VariableUiElement(tags.map(tags => { - const parts = []; - for (const key in tags) { - parts.push(key+"="+tags[key]); - } - return parts.join("
") - })).SetStyle("border: 1px solid black; border-radius: 1em;padding:1em;display:block;") - }) - } + ] static HelpMessage: UIElement = SpecialVisualizations.GenHelpMessage(); diff --git a/UI/UserBadge.ts b/UI/UserBadge.ts index 49e8acf..04ffcd9 100644 --- a/UI/UserBadge.ts +++ b/UI/UserBadge.ts @@ -34,7 +34,7 @@ export class UserBadge extends UIElement { .SetClass("userbadge-login") .onClick(() => State.state.osmConnection.AttemptLogin()); this._logout = - Svg.logout_ui() + Svg.logout_svg() .onClick(() => { State.state.osmConnection.LogOut(); }); @@ -52,7 +52,7 @@ export class UserBadge extends UIElement { this._homeButton = new VariableUiElement( this._userDetails.map((userinfo) => { if (userinfo.home) { - return Svg.home_img; + return Svg.home; } return ""; }) @@ -75,7 +75,7 @@ export class UserBadge extends UIElement { let messageSpan: UIElement = new Link( - new Combine([Svg.envelope_img, "" + user.totalMessages]), + new Combine([Svg.envelope, "" + user.totalMessages]), 'https://www.openstreetmap.org/messages/inbox', true ) @@ -83,7 +83,7 @@ export class UserBadge extends UIElement { if (user.unreadMessages > 0) { messageSpan = new Link( - new Combine([Svg.envelope_img, "" + user.unreadMessages]), + new Combine([Svg.envelope, "" + user.unreadMessages]), 'https://www.openstreetmap.org/messages/inbox', true ).SetClass("alert") @@ -94,17 +94,8 @@ export class UserBadge extends UIElement { dryrun = new FixedUiElement("TESTING").SetClass("alert"); } - if (user.home !== undefined) { - const icon = L.icon({ - iconUrl: Img.AsData(Svg.home), - iconSize: [20, 20], - iconAnchor: [10, 10] - }); - L.marker([user.home.lat, user.home.lon], {icon: icon}).addTo(State.state.bm.map); - } - const settings = - new Link(Svg.gear_ui(), + new Link(Svg.gear_svg(), `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`, true) @@ -124,7 +115,7 @@ export class UserBadge extends UIElement { const csCount = new Link( - new Combine([Svg.star_img, "" + user.csCount]), + new Combine([Svg.star, "" + user.csCount]), `https://www.openstreetmap.org/user/${user.name}/history`, true); diff --git a/UI/WelcomeMessage.ts b/UI/WelcomeMessage.ts index 8fe2c00..4179285 100644 --- a/UI/WelcomeMessage.ts +++ b/UI/WelcomeMessage.ts @@ -24,9 +24,10 @@ export class WelcomeMessage extends UIElement { this.description = new Combine([ "

", layout.title, "

", layout.description - ]) layout.descriptionTail + + this.plzLogIn = diff --git a/Utils.ts b/Utils.ts index 9e5cdf7..a0b9815 100644 --- a/Utils.ts +++ b/Utils.ts @@ -3,6 +3,7 @@ import * as $ from "jquery" export class Utils { + public static readonly assets_path = "./assets/svg/"; static EncodeXmlValue(str) { return str.replace(/&/g, '&') @@ -154,5 +155,19 @@ export class Utils { }); } + + public static LoadCustomCss(location: string){ + var head = document.getElementsByTagName('head')[0]; + var link = document.createElement('link'); + link.id = "customCss"; + link.rel = 'stylesheet'; + link.type = 'text/css'; + link.href = location; + link.media = 'all'; + head.appendChild(link); + console.log("Added custom layout ",location) + } + + } diff --git a/assets/layers/direction/direction.json b/assets/layers/direction/direction.json new file mode 100644 index 0000000..f153319 --- /dev/null +++ b/assets/layers/direction/direction.json @@ -0,0 +1,32 @@ +{ + "id": "direction", + "name": { + "en": "Direction visualization" + }, + "minzoom": 16, + "overpassTags": { + "or": ["camera:direction~*","direction~*"] + }, + "doNotDownload": true, + "passAllFeatures": true, + "title": null, + "description": { + "en": "This layer visualizes directions" + }, + "tagRenderings": [], + "icon": "./assets/svg/direction_gradient.svg", + "rotation": { + "render": "{camera:direction}deg", + "mappings": [ + { + "if": "direction~*", + "then": "{direction}deg" + } + ] + }, + "iconSize": "200,200,center", + "color": "--catch-detail-color", + "stroke": "0", + "presets": [], + "wayHandling": 2 +} \ No newline at end of file diff --git a/assets/layers/ghost_bike/ghost_bike.json b/assets/layers/ghost_bike/ghost_bike.json index fce6998..d33d7df 100644 --- a/assets/layers/ghost_bike/ghost_bike.json +++ b/assets/layers/ghost_bike/ghost_bike.json @@ -43,8 +43,7 @@ } ], "tagRenderings": [ - "images", - { + { "render": { "en": "A ghost bike is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.", "nl": "Een Witte Fiets (of Spookfiets) is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat over een witgeschilderde fiets die geplaatst werd in de buurt van het ongeval.", diff --git a/assets/layers/surveillance_cameras/surveillance_cameras.json b/assets/layers/surveillance_cameras/surveillance_cameras.json new file mode 100644 index 0000000..bf4c430 --- /dev/null +++ b/assets/layers/surveillance_cameras/surveillance_cameras.json @@ -0,0 +1,379 @@ +{ + "id": "surveillance_cameras", + "name": { + "en": "Surveillance camera's", + "nl": "Bewakingscamera's" + }, + "minzoom": 12, + "overpassTags": { + "and": [ + "man_made=surveillance", + { + "or": [ + "surveillance:type=camera", + "surveillance:type=ALPR", + "surveillance:type=ANPR" + ] + } + ] + }, + "title": { + "render": { + "en": "Surveillance Camera", + "nl": "Bewakingscamera" + } + }, + "description": {}, + "tagRenderings": [ + "images", + { + "#": "Camera type: fixed; panning; dome", + "question": { + "en": "What kind of camera is this?", + "nl": "Wat voor soort camera is dit?" + }, + "mappings": [ + { + "if": { + "and": [ + "camera:type=fixed" + ] + }, + "then": { + "en": "A fixed (non-moving) camera", + "nl": "Een vaste camera" + } + }, + { + "if": { + "and": [ + "camera:type=dome" + ] + }, + "then": { + "en": "A dome camera (which can turn)", + "nl": "Een dome (bolvormige camera die kan draaien)" + } + }, + { + "if": { + "and": [ + "camera:type=panning" + ] + }, + "then": { + "en": "A panning camera", + "nl": "Een camera die (met een motor) van links naar rechts kan draaien" + } + } + ] + }, + { + "#": "direction. We don't ask this for a dome on a pole or ceiling as it has a 360° view", + "question": { + "en": "In which geographical direction does this camera film?", + "nl": "Naar welke geografische richting filmt deze camera?" + }, + "render": "Films to {camera:direction}", + "condition": { + "or": [ + "camera:type!=dome", + { + "and": [ + "camera:mount!=ceiling", + "camera:mount!=pole" + ] + } + ] + }, + "freeform": { + "key": "camera:direction", + "type": "direction" + } + }, + { + "#": "Operator", + "freeform": { + "key": "operator" + }, + "question": { + "en": "Who operates this CCTV?", + "nl": "Wie beheert deze bewakingscamera?" + }, + "render": { + "en": "Operated by {operator}", + "nl": "Beheer door {operator}" + } + }, + { + "#": "Surveillance type: public, outdoor, indoor", + "question": { + "en": "What kind of surveillance is this camera", + "nl": "Wat soort bewaking wordt hier uitgevoerd?" + }, + "mappings": [ + { + "if": { + "and": [ + "surveillance=public" + ] + }, + "then": { + "en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station, a public corridor or tunnel,...", + "nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw, een publiek toegankelijke gang of tunnel..." + } + }, + { + "if": { + "and": [ + "surveillance=outdoor" + ] + }, + "then": { + "en": "An outdoor, yet private area is surveilled (e.g. a parking lot, a fuel station, courtyard, entrance, private driveway, ...)", + "nl": "Een buitenruimte met privaat karakter (zoals een privé-oprit, een parking, tankstation, ...)" + } + }, + { + "if": { + "and": [ + "surveillance=indoor" + ] + }, + "then": { + "nl": "Een private binnenruimte wordt bewaakt, bv. een winkel, een parkeergarage, ...", + "en": "A private indoor area is surveilled, e.g. a shop, a private underground parking, ..." + } + } + ] + }, + { + "#": "Indoor camera? This isn't clear for 'public'-cameras", + "question": { + "en": "Is the public space surveilled by this camera an indoor or outdoor space?", + "nl": "Bevindt de bewaakte publieke ruimte camera zich binnen of buiten?" + }, + "condition": { + "and": [ + "surveillance:type=public" + ] + }, + "mappings": [ + { + "if": "indoor=yes", + "then": { + "en": "This camera is located indoors", + "nl": "Deze camera bevindt zich binnen" + } + }, + { + "if": "indoor=no", + "then": { + "en": "This camera is located outdoors", + "nl": "Deze camera bevindt zich buiten" + } + }, + { + "if": "indoor=", + "then": { + "en": "This camera is probably located outdoors", + "nl": "Deze camera bevindt zich waarschijnlijk buiten" + }, + "hideInAnswer": true + } + ] + }, + { + "#": "Level", + "question": { + "en": "On which level is this camera located?", + "nl": "Op welke verdieping bevindt deze camera zich?" + }, + "freeform": { + "key": "level", + "type": "nat" + }, + "condition": { + "or": [ + "indoor=yes", + "surveillance:type=ye" + ] + } + }, + { + "#": "Surveillance:zone", + "question": { + "en": "What exactly is surveilled here?", + "nl": "Wat wordt hier precies bewaakt?" + }, + "freeform": { + "key": "surveillance:zone" + }, + "render": { + "en": " Surveills a {surveillance:zone}", + "nl": "Bewaakt een {surveillance:zone}" + }, + "mappings": [ + { + "if": { + "and": [ + "surveillance:zone=parking" + ] + }, + "then": { + "en": "Surveills a parking", + "nl": "Bewaakt een parking" + } + }, + { + "if": { + "and": [ + "surveillance:zone=traffic" + ] + }, + "then": { + "en": "Surveills the traffic", + "nl": "Bewaakt het verkeer" + } + }, + { + "if": { + "and": [ + "surveillance:zone=entrance" + ] + }, + "then": { + "en": "Surveills an entrance", + "nl": "Bewaakt een ingang" + } + }, + { + "if": { + "and": [ + "surveillance:zone=corridor" + ] + }, + "then": { + "en": "Surveills a corridor", + "nl": "Bewaakt een gang" + } + }, + { + "if": { + "and": [ + "surveillance:zone=public_transport_platform" + ] + }, + "then": { + "en": "Surveills a public tranport platform", + "nl": "Bewaakt een perron of bushalte" + } + }, + { + "if": { + "and": [ + "surveillance:zone=shop" + ] + }, + "then": { + "en": "Surveills a shop", + "nl": "Bewaakt een winkel" + } + } + ] + }, + { + "#": "camera:mount", + "question": { + "en": "How is this camera placed?", + "nl": "Hoe is deze camera geplaatst?" + }, + "freeform": { + "key": "camera:mount" + }, + "mappings": [ + { + "if": "camera:mount=wall", + "then": { + "en": "This camera is placed against a wall", + "nl": "Deze camera hangt aan een muur" + } + }, + { + "if": "camera:mount=pole", + "then": { + "en": "This camera is placed one a pole", + "nl": "Deze camera staat op een paal" + } + }, + { + "if": "camera:mount=ceiling", + "then": { + "en": "This camera is placed on the ceiling", + "nl": "Deze camera hangt aan het plafond" + } + } + ] + } + ], + "hideUnderlayingFeaturesMinPercentage": 0, + "icon": { + "render": "./assets/themes/surveillance_cameras/logo.svg", + "mappings": [ + { + "if": "camera:type=dome", + "then": "./assets/themes/surveillance_cameras/dome.svg" + }, + { + "if": "_direction:leftright=right", + "then": "./assets/themes/surveillance_cameras/cam_right.svg" + }, + { + "if": "_direction:leftright=left", + "then": "./assets/themes/surveillance_cameras/cam_left.svg" + } + ] + }, + "rotation": { + "render": "calc({camera:direction}deg + 90deg)", + "mappings": [ + { + "if": "camera:type=dome", + "then": "0" + }, + { + "if": "_direction:leftright=right", + "then": "calc({camera:direction}deg - 90deg)" + } + ] + }, + "width": { + "render": "8" + }, + "iconSize": { + "mappings": [ + { + "if": "camera:type=dome", + "then": "50,50,center" + }, + { + "if": "_direction:leftright~*", + "then": "100,35,center" + } + ], + "render": "50,50,center" + }, + "color": { + "render": "#f00" + }, + "presets": [ + { + "tags": [ + "man_made=surveillance", + "surveillance:type=camera" + ], + "title": "Surveillance camera" + } + ], + "wayHandling": 2 +} \ No newline at end of file diff --git a/assets/questions/questions.json b/assets/questions/questions.json index 678df97..2595eab 100644 --- a/assets/questions/questions.json +++ b/assets/questions/questions.json @@ -4,7 +4,7 @@ }, "osmlink": { - "render": "", + "render": "", "mappings":[{ "if": "id~=-", "then": "Uploading..." diff --git a/assets/svg/ampersand.svg b/assets/svg/ampersand.svg index 525a1ef..f2df861 100644 --- a/assets/svg/ampersand.svg +++ b/assets/svg/ampersand.svg @@ -1,4 +1,4 @@ - +e diff --git a/assets/svg/checkmark.svg b/assets/svg/checkmark.svg index 59d073f..e206d63 100644 --- a/assets/svg/checkmark.svg +++ b/assets/svg/checkmark.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/assets/svg/compass.svg b/assets/svg/compass.svg new file mode 100644 index 0000000..52f0b97 --- /dev/null +++ b/assets/svg/compass.svg @@ -0,0 +1,199 @@ + + + + + + + image/svg+xml + + + + + + + N + S + E + W + NW + SW + NE + SE + diff --git a/assets/svg/crosshair.svg b/assets/svg/crosshair.svg index ff7f105..9705586 100644 --- a/assets/svg/crosshair.svg +++ b/assets/svg/crosshair.svg @@ -1,55 +1,17 @@ - - + version="1.1" + viewBox="0 0 26.458333 26.458334" + height="100" + width="100"> - - - - @@ -58,46 +20,40 @@ image/svg+xml - + + transform="translate(0,-270.54165)" + id="layer1"> + id="path815" + style="fill:none;fill-opacity:1;stroke:#000000;stroke-width:2.64583335;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:0.98823529" /> + d="M 3.2841366,283.77082 H 1.0418969" + style="fill:none;stroke:#000000;stroke-width:2.09723878;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> + d="M 25.405696,283.77082 H 23.286471" + style="fill:none;stroke:#000000;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> + d="m 13.229167,295.9489 v -2.11763" + style="fill:none;stroke:#000000;stroke-width:2.11666679;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> + d="m 13.229167,275.05759 v -3.44507" + style="fill:none;stroke:#000000;stroke-width:2.11666668;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.98823529" /> + cx="13.229166" + id="path866" + style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:2.81138086;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" /> diff --git a/assets/svg/direction.svg b/assets/svg/direction.svg new file mode 100644 index 0000000..e1eccac --- /dev/null +++ b/assets/svg/direction.svg @@ -0,0 +1,31 @@ + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/svg/direction_gradient.svg b/assets/svg/direction_gradient.svg new file mode 100644 index 0000000..8ba4912 --- /dev/null +++ b/assets/svg/direction_gradient.svg @@ -0,0 +1,92 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + + + + + + diff --git a/assets/svg/envelope.svg b/assets/svg/envelope.svg index 8728151..74923d3 100644 --- a/assets/svg/envelope.svg +++ b/assets/svg/envelope.svg @@ -1,4 +1,47 @@ - - - - \ No newline at end of file + + + + + + image/svg+xml + + + + + + + + + + + + diff --git a/assets/svg/gear.svg b/assets/svg/gear.svg index 302fac4..6142f34 100644 --- a/assets/svg/gear.svg +++ b/assets/svg/gear.svg @@ -1 +1,4 @@ - \ No newline at end of file + + + + \ No newline at end of file diff --git a/assets/svg/help.svg b/assets/svg/help.svg index 1f6bad7..b6bd681 100644 --- a/assets/svg/help.svg +++ b/assets/svg/help.svg @@ -8,33 +8,21 @@ id="svg11382" height="900" width="900" + viewBox="0 0 900 900" version="1.0"> - - - - image/svg+xml - - - - - - + transform="matrix(0.90103258,0,0,0.90103258,112.84058,-1.9060177)" + > diff --git a/assets/svg/home.svg b/assets/svg/home.svg index b9f31cf..ca3ede2 100644 --- a/assets/svg/home.svg +++ b/assets/svg/home.svg @@ -1,3 +1,6 @@ - \ No newline at end of file + + + \ No newline at end of file diff --git a/assets/svg/home_white_bg.svg b/assets/svg/home_white_bg.svg new file mode 100644 index 0000000..59babf8 --- /dev/null +++ b/assets/svg/home_white_bg.svg @@ -0,0 +1,29 @@ + +image/svg+xml + + + \ No newline at end of file diff --git a/assets/svg/pencil.svg b/assets/svg/pencil.svg index 6f9610f..d41508f 100644 --- a/assets/svg/pencil.svg +++ b/assets/svg/pencil.svg @@ -1,3 +1,3 @@ - + \ No newline at end of file diff --git a/assets/svg/share.svg b/assets/svg/share.svg index d70d175..83f599f 100644 --- a/assets/svg/share.svg +++ b/assets/svg/share.svg @@ -73,25 +73,25 @@ id="layer1" transform="translate(0,-270.54165)">
Natuur maakt gelukkig. Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten. \n
  • In welke natuurgebieden kan jij terecht? Hoe toegankelijk zijn ze?
  • In welke bossen kan een gezin in jouw gemeente opnieuw op adem komen?
  • Op welke onbekende plekjes is het zalig spelen?

Samen kleuren we heel Vlaanderen en Brussel groen.

Blijf op de hoogte van de resultaten van buurtnatuur.be: meld je aan voor e-mailupdates." + "nl": "logo-groenmeld je aan voor e-mailupdates." }, "descriptionTail": { "nl": "

Tips

  • Over groen ingekleurde gebieden weten we alles wat we willen weten.
  • Bij rood ingekleurde gebieden ontbreekt nog heel wat info: klik een gebied aan en beantwoord de vragen.
  • Je kan altijd een vraag overslaan als je het antwoord niet weet of niet zeker bent
  • Je kan altijd een foto toevoegen
  • Je kan ook zelf een gebied toevoegen door op de kaart te klikken
  • Open buurtnatuur.be op je smartphone om al wandelend foto's te maken en vragen te beantwoorden

De oorspronkelijke data komt van OpenStreetMap en je antwoorden worden daar bewaard.
Omdat iedereen vrij kan meewerken aan dit project, kunnen we niet garanderen dat er geen fouten opduiken.Kan je hier niet aanpassen wat je wilt, dan kan je dat zelf via OpenStreetMap.org doen. Groen kan geen enkele verantwoordelijkheid nemen over de kaart.

Je privacy is belangrijk. We tellen wel hoeveel gebruikers deze website bezoeken. We plaatsen een cookie waar geen persoonlijke informatie in bewaard wordt. Als je inlogt, komt er een tweede cookie bij met je inloggegevens.
" diff --git a/assets/themes/surveillance_cameras/cam_left.svg b/assets/themes/surveillance_cameras/cam_left.svg new file mode 100644 index 0000000..aa8a983 --- /dev/null +++ b/assets/themes/surveillance_cameras/cam_left.svg @@ -0,0 +1,63 @@ + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/themes/surveillance_cameras/cam_right.svg b/assets/themes/surveillance_cameras/cam_right.svg new file mode 100644 index 0000000..78fd86f --- /dev/null +++ b/assets/themes/surveillance_cameras/cam_right.svg @@ -0,0 +1,68 @@ + + + + + + + + + + image/svg+xml + + + + + + + + diff --git a/assets/themes/surveillance_cameras/custom_theme.css b/assets/themes/surveillance_cameras/custom_theme.css new file mode 100644 index 0000000..97a49d8 --- /dev/null +++ b/assets/themes/surveillance_cameras/custom_theme.css @@ -0,0 +1,26 @@ +html { + --subtle-detail-color: #2c2 !important; + --subtle-detail-color-contrast: white !important; + --popup-border: #00ff00 !important; + --catch-detail-color: #00ff00 !important; + --catch-detail-color-contrast: black !important; + --alert-color: #eb00ff !important; + --background-color: black !important; + --foreground-color: white !important; + --shadow-color: #0f0 !important; +} + +#innercolor { + stop-color:#ff0000 +} +.leaflet-div-icon svg { + width: calc(100% - 3px); + height: calc(100% + 3px); +} +/* +.leaflet-div-icon svg path { + fill: none !important; + stroke-width: 1px !important; + stroke: #0f0 !important; +} +*/ diff --git a/assets/themes/surveillance_cameras/direction_360.svg b/assets/themes/surveillance_cameras/direction_360.svg new file mode 100644 index 0000000..fc60a9a --- /dev/null +++ b/assets/themes/surveillance_cameras/direction_360.svg @@ -0,0 +1,78 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + diff --git a/assets/themes/surveillance_cameras/dome.svg b/assets/themes/surveillance_cameras/dome.svg new file mode 100644 index 0000000..b1e7eea --- /dev/null +++ b/assets/themes/surveillance_cameras/dome.svg @@ -0,0 +1,93 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + diff --git a/assets/themes/surveillance_cameras/logo.svg b/assets/themes/surveillance_cameras/logo.svg new file mode 100644 index 0000000..2ae0f32 --- /dev/null +++ b/assets/themes/surveillance_cameras/logo.svg @@ -0,0 +1,62 @@ + + + + + + image/svg+xml + + + + + + + + + diff --git a/assets/themes/surveillance_cameras/surveillance_cameras.json b/assets/themes/surveillance_cameras/surveillance_cameras.json new file mode 100644 index 0000000..5495864 --- /dev/null +++ b/assets/themes/surveillance_cameras/surveillance_cameras.json @@ -0,0 +1,34 @@ +{ + "id": "surveillance", + "title": { + "en": "Surveillance under Surveillance", + "nl": "Surveillance under Surveillance" + }, + "shortDescription": { + "en": "Surveillance cameras and other means of surveillance", + "nl": "Bewakingscameras en dergelijke" + }, + "description": { + "en": "On this open map, you can find surveillance cameras.", + "nl": "Op deze open kaart kan je bewakingscamera's vinden." + }, + "language": [ + "en", + "nl" + ], + "maintainer": "", + "icon": "./assets/themes/surveillance_cameras/logo.svg", + "version": "0", + "startLat": 0, + "startLon": 0, + "startZoom": 1, + "widenFactor": 0.05, + "socialImage": "", + "customCss": "./assets/themes/surveillance_cameras/custom_theme.css", + "defaultBackgroundId": "Stadia.AlidadeSmoothDark", + "layers": [ + "direction", + "surveillance_cameras" + ], + "roamingRenderings": [] +} \ No newline at end of file diff --git a/css/imageUploadFlow.css b/css/imageUploadFlow.css new file mode 100644 index 0000000..c36b33b --- /dev/null +++ b/css/imageUploadFlow.css @@ -0,0 +1,23 @@ +.image-upload-flow-button span { + width: max-content; + font-size: 28px; + font-weight: bold; + margin-top: 4px; + padding-top: 4px; + padding-bottom: 4px; + padding-left: 13px; +} + +.image-upload-flow-button { + display: flex; + cursor: pointer; + padding: 0.5em; + border-radius: 1em; + border: 3px solid var(--foreground-color); + box-sizing: border-box; +} + +.image-upload-flow svg { + fill: var(--foreground-color); + stroke: var(--foreground-color); +} diff --git a/css/mobile.css b/css/mobile.css index f4c5839..8525b13 100644 --- a/css/mobile.css +++ b/css/mobile.css @@ -4,6 +4,8 @@ Contains tweaks for small screens .only-on-mobile { display: none; + background-color: var(--background-color); + color: var(--foreground-color); } @media only screen and (max-width: 600px), only screen and (max-height: 600px) { @@ -14,6 +16,8 @@ Contains tweaks for small screens .only-on-mobile { display: unset; + background-color: var(--background-color); + color: var(--foreground-color); } .hidden-on-mobile { @@ -22,6 +26,8 @@ Contains tweaks for small screens #messagesbox { display: none; + background-color: var(--background-color); + color: var(--foreground-color); } #help-button-mobile{ @@ -37,10 +43,11 @@ Contains tweaks for small screens #geolocate-button { display: block; } - + .leaflet-popup { - /* Popups are hidden on mobile */ + /* On mobile, the popups are shown as a full-screen element */ display: none; + visibility: hidden; } #centermessage { @@ -57,15 +64,15 @@ Contains tweaks for small screens #messagesboxmobile { display: block; + position: absolute; z-index: 10000; - background-color: white; width: 100vw; } #welcomeMessage { display: inline-block; - background-color: white; + background-color: var(--background-color); border-radius: 0; width: 100%; max-width: 100%; diff --git a/css/openinghourstable.css b/css/openinghourstable.css index 5034cff..4d04de0 100644 --- a/css/openinghourstable.css +++ b/css/openinghourstable.css @@ -5,7 +5,6 @@ text-align: center; word-break: normal; } - .oh-table th { padding: 0; margin: 0; @@ -34,22 +33,22 @@ } .oh-timecell:hover { - background-color: #92b1ff !important; + background-color: var(--catch-detail-color) !important; } .oh-timecell-selected { - background-color: #0048ff; + background-color: var(--catch-detail-color); } .oh-timecell-half { border-bottom: 1px solid #ddd; - background-color: aliceblue; + background-color: var(--subtle-detail-color); } .oh-timecell-half.oh-timecell-selected { - background-color: #0048ff; + background-color: var(--catch-detail-color); } .oh-table tr { @@ -75,12 +74,12 @@ } .oh-timecol-selected { - border-right: #0048ff; + border-right: var(--catch-detail-color); } .oh-timecol-selected > span { - background-color: #0048ff; - color: white; + background-color: var(--catch-detail-color); + color: var(--background-color); width: 100%; display: block; } @@ -96,11 +95,11 @@ } .oh-timerow-selected .oh-timecell-0 { - border-left: 10px solid #0048ff !important; + border-left: 10px solid var(--catch-detail-color) !important; } .oh-timerow-selected .oh-timecell-6 { - border-right: 10px solid #0048ff !important; + border-right: 10px solid var(--catch-detail-color) !important; } @@ -117,7 +116,7 @@ top: 0; left: 0; width: calc(100% - 4px); - background: #0048ff; + background: var(--catch-detail-color); z-index: 1; box-sizing: border-box; } @@ -276,7 +275,7 @@ .ohviz-today { - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); } .ohviz-weekday { @@ -290,7 +289,7 @@ } .ohviz-container { - border: 0.5em solid #e5f5ff; + border: 0.5em solid var(--subtle-detail-color); border-radius: 1em; display: block; } diff --git a/css/tabbedComponent.css b/css/tabbedComponent.css index 741c7ba..d895fc9 100644 --- a/css/tabbedComponent.css +++ b/css/tabbedComponent.css @@ -7,7 +7,7 @@ flex-wrap: nowrap; justify-content: flex-start; align-items: start; - background-color: white; + background-color: var(--background-color); max-width: 100vw; overflow-x: auto; } @@ -21,10 +21,19 @@ margin:auto; } +.tab-single-header svg { + height: 3em; + max-width: 3em; + padding: 0.5em; + display:block; + margin:auto; +} + .tab-content { z-index: 5002; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); position: relative; padding: 1em; display: inline-block; @@ -41,19 +50,36 @@ } .tab-active { - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); z-index: 5001; - box-shadow: 0 0 10px black; - border: 1px solid white; + box-shadow: 0 0 10px var(--shadow-color); + border: 1px solid var(--background-color); min-width: 4em; } +.tab-active svg { + fill: var(--foreground-color); + stroke: var(--foreground-color); +} + .tab-non-active { - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); + color: var(--foreground-color); opacity: 0.5; border-left: 1px solid gray; - border-right:1px solid gray; - border-top: 1px solid gray; + border-right: 1px solid gray; + border-top: 1px solid gray; border-bottom: 1px solid lightgray; min-width: 4em; } + +.tab-non-active svg { + fill: var(--foreground-color) !important; + stroke: var(--foreground-color) !important; +} + +.tab-non-active svg path{ + fill: var(--foreground-color) !important; + stroke: var(--foreground-color) !important; +} diff --git a/css/tagrendering.css b/css/tagrendering.css index 7c08861..aa82b69 100644 --- a/css/tagrendering.css +++ b/css/tagrendering.css @@ -30,13 +30,19 @@ .question { display: block; margin-top: 1em; - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); padding: 1em; border-radius: 1em; font-size: larger; } +.question svg { + width: 100%; + height: 100%; +} + .question-text { font-size: larger; font-weight: bold; @@ -65,15 +71,15 @@ } input:checked + label .question-option-with-border { - border: 2px solid black; + border: 2px solid var(--subtle-detail-color-contrast); } .save { display: inline-block; - border: solid white 2px; - background-color: #3a3aeb; - color: white; + border: solid var(--catch-detail-color-contrast) 2px; + background-color: var(--catch-detail-color); + color: var(--catch-detail-color-contrast); padding: 0.2em 0.6em; font-size: x-large; font-weight: bold; @@ -89,9 +95,9 @@ input:checked + label .question-option-with-border { .login-button-friendly { display: inline-block; - border: solid white 2px; - background-color: #3a3aeb; - color: white; + background-color:var(--catch-detail-color); + color: var(--catch-detail-color-contrast); + border: solid var(--catch-detail-color-contrast) 2px; padding: 0.2em 0.6em; font-size: large; font-weight: bold; @@ -115,7 +121,24 @@ input:checked + label .question-option-with-border { height: 1.3em; padding: 0.5em; border-radius: 0.65em; - border: solid black 1px; + border: solid var(--popup-border) 1px; font-size: medium; float: right; +} + +.edit-button svg { + width: 1.3em; + height: 1.3em; + padding: 0.5em; + border-radius: 0.65em; + border: solid var(--foreground-color) 1px; + stroke: var(--foreground-color) !important; + fill: var(--foreground-color) !important; + font-size: medium; + float: right; +} + +.edit-button svg path{ + stroke: var(--foreground-color) !important; + fill: var(--foreground-color) !important; } \ No newline at end of file diff --git a/css/userbadge.css b/css/userbadge.css index 81ac712..ca36f51 100644 --- a/css/userbadge.css +++ b/css/userbadge.css @@ -1,6 +1,7 @@ #userbadge { display: inline-block; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); -webkit-border-radius: 2em; -moz-border-radius: 2em; border-radius: 2em; @@ -15,7 +16,7 @@ #userbadge a { text-decoration: none; - color: black; + color: var(--foreground-color); } @@ -42,14 +43,19 @@ margin-bottom: 0.2em; } -.userstats img { +.userstats svg { width: 1em; height: 1em; - fill: black; border-radius: 0; display: block; } +.userstats img { + width: 1em; + height: 1em; + border-radius: 0; + display: block; +} #profile-pic { float: left; @@ -76,7 +82,8 @@ height: 2.2em; /*SHould equal profile-pic height - padding*/ z-index: 5000; text-align: left; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); background-size: 100%; line-height: 0.75em; @@ -88,7 +95,8 @@ .userbadge-login { font-weight: bold; font-size: large; - background-color: #e5f5ff !important; + background-color: var(--subtle-detail-color) !important; + color: var(--subtle-detail-color-contrast); height:3em; display: inline-block; diff --git a/index.css b/index.css index 135578a..ed793ac 100644 --- a/index.css +++ b/index.css @@ -1,11 +1,42 @@ +:root { + --subtle-detail-color: #e5f5ff; + --subtle-detail-color-contrast: black; + --catch-detail-color: #3a3aeb; + --catch-detail-color-contrast: white; + --alert-color: #fee4d1; + --background-color: white; + --foreground-color: black; + --popup-border: white; + --shadow-color: #00000066; + + --return-to-the-map-height: 5em; +} + html, body { height: 100%; margin: 0; padding: 0; + background-color: var(--background-color); + color: var(--foreground-color); + font-family: 'Helvetica Neue', Arial, sans-serif; } -body { - font-family: 'Helvetica Neue', Arial, sans-serif; +a { + color: var(--foreground-color) +} + +#topleft-tools svg { + fill: var(--foreground-color) !important; + stroke: var(--foreground-color) !important; +} + +#topleft-tools svg path { + fill: var(--foreground-color) !important; + stroke: var(--foreground-color) !important; +} + +.direction-svg svg path{ + fill: var(--catch-detail-color) !important; } @@ -13,6 +44,23 @@ body { height: 100%; } +.leaflet-popup-content-wrapper { + background-color: var(--background-color); + color: var(--foreground-color); + border: 2px solid var(--popup-border); + box-shadow: 0 3px 14px var(--shadow-color) !important; +} + +.leaflet-container { + background-color: var(--background-color) !important; +} + +.leaflet-popup-tip { + background-color: var(--popup-border) !important; + color: var(--popup-border) !important; + box-shadow: 0 3px 14px var(--shadow-color) !important; +} + #geolocate-button { position: absolute; bottom: 25px; @@ -39,30 +87,33 @@ body { bottom: 1em; left: 1em; z-index: 9000; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); + border-radius: 1em; cursor: pointer; - box-shadow: 0 0 10px #00000066; + box-shadow: 0 0 10px var(--shadow-color); } .layer-selection-toggle { - border-top-left-radius: 1em; - border-bottom-left-radius: 1em; + border-radius: 1em; display: flex; flex-direction: column-reverse; + background: var(--subtle-detail-color); } -.layer-selection-toggle img { +.layer-selection-toggle svg { display: block; width: 2em; - padding: 1em; + height: 2em; + padding: 0.75em; } /**************** GENERIC ****************/ .alert { - background-color: #fee4d1; + background-color: var(--alert-color); font-weight: bold; border-radius: 1em; margin: 0.25em; @@ -82,7 +133,7 @@ body { } .shadow { - box-shadow: 0 0 10px #00000066; + box-shadow: 0 0 10px var(--shadow-color); } .title-font span { @@ -91,7 +142,8 @@ body { } .soft { - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); font-weight: bold; border-radius: 1em; margin: 0.25em; @@ -121,6 +173,10 @@ body { pointer-events: all; } +.unclickable { + pointer-events: none !important; +} + .page-split { display: flex; height: 100%; @@ -137,7 +193,9 @@ body { #searchbox { display: inline-block; text-align: left; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); + transition: all 500ms linear; pointer-events: all; border-radius: 1.3em; @@ -170,6 +228,7 @@ body { font-size: large; width: 100%; box-sizing: border-box; + color: var(--foreground-color); } .search-go img { @@ -200,7 +259,6 @@ body { position: absolute; z-index: 5000; transition: all 500ms linear; - overflow-x: hidden; pointer-events: none; /* Shadow offset */ padding: 0.5em 10px 0 0.5em; @@ -215,13 +273,16 @@ body { overflow-y: auto; border-top-right-radius: 1em; border-bottom-right-radius: 1em; + background-color: var(--background-color); + color: var(--foreground-color); } .close-welcome-button { position: absolute; display: inline-block; height: 100%; - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); box-sizing: border-box; width: 4em; padding: 1em; @@ -229,22 +290,26 @@ body { border-bottom-left-radius: 1em; } -.close-welcome-button img { +.close-welcome-button svg { width: 2em; + height: 2em; } .open-welcome-button { display: inline-block; box-sizing: border-box; - background-color: white; - height: 4em; - width: 4em; - padding: 1em; + background: var(--subtle-detail-color); + color: var(--foreground-color); + + height: 3.5em; + width: 3.5em; + padding: 0.75em; border-radius: 1em; } -.open-welcome-button img { +.open-welcome-button svg { width: 2em; + height: 2em; } #messagesbox { @@ -252,9 +317,11 @@ body { position: relative; padding: 0; pointer-events: all; - box-shadow: 0 0 10px #00000066; + box-shadow: 0 0 10px var(--shadow-color); border-radius: 1em; - width: min-content + width: min-content; + background-color: var(--background-color); + color: var(--foreground-color); } @@ -273,7 +340,9 @@ body { pointer-events: none; opacity: 1; - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); + transition: opacity 500ms linear; @@ -344,6 +413,13 @@ body { overflow-y: auto; overflow-x: hidden; } + +.leaflet-div-icon { + background-color: unset !important; + border: unset !important; +} + + /****** ShareScreen *****/ .literal-code { @@ -357,7 +433,9 @@ body { .iframe-escape { - background-color: white; + background-color: var(--background-color); + color: var(--foreground-color); + border-radius: 2em; display: block; width: min-content; @@ -376,11 +454,11 @@ body { flex-direction: row; font-size: large; margin: 0.5em; - background-color: #e5f5ff; + background-color: var(--subtle-detail-color); + color: var(--subtle-detail-color-contrast); border-radius: 1em; align-items: center; text-decoration: none; - color: black; } @@ -416,3 +494,8 @@ body { } +.small-image img { + height: 1em; + max-width: 1em; +} + diff --git a/index.html b/index.html index 70be6c3..3751ce6 100644 --- a/index.html +++ b/index.html @@ -13,6 +13,7 @@ + diff --git a/index.ts b/index.ts index 712f570..d117565 100644 --- a/index.ts +++ b/index.ts @@ -4,8 +4,8 @@ import {InitUiElements} from "./InitUiElements"; import {QueryParameters} from "./Logic/Web/QueryParameters"; import {UIEventSource} from "./Logic/UIEventSource"; import * as $ from "jquery"; -import SharedLayers from "./Customizations/SharedLayers"; import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import {Utils} from "./Utils"; let defaultLayout = "bookcases" // --------------------- Special actions based on the parameters ----------------- @@ -30,6 +30,10 @@ if(location.href.indexOf("pietervdvn.github.io") >= 0){ defaultLayout = "bookcases" } +const customCssQP = QueryParameters.GetQueryParameter("custom-css", "", "If specified, the custom css from the given link will be loaded additionaly"); +if(customCssQP.data !== undefined && customCssQP.data !== ""){ + Utils.LoadCustomCss(customCssQP.data); +} let testing: UIEventSource; @@ -54,23 +58,7 @@ if (path !== "index.html" && path !== "") { defaultLayout = path.substr(0, path.length - 5); console.log("Using layout", defaultLayout); } - -// Run over all questsets. If a part of the URL matches a searched-for part in the layout, it'll take that as the default -for (const k in AllKnownLayouts.allSets) { - const layout : LayoutConfig= AllKnownLayouts.allSets[k]; - const possibleParts = (layout.locationContains ?? []); - for (const locationMatch of possibleParts) { - if (locationMatch === "") { - continue - } - if (window.location.href.toLowerCase().indexOf(locationMatch.toLowerCase()) >= 0) { - defaultLayout = layout.name; - } - } -} - -defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout).data; - +defaultLayout = QueryParameters.GetQueryParameter("layout", defaultLayout,"The layout to load into MapComplete").data; let layoutToUse: LayoutConfig = AllKnownLayouts.allSets[defaultLayout.toLowerCase()] ?? AllKnownLayouts["all"]; @@ -86,18 +74,20 @@ if (layoutFromBase64.startsWith("wiki:")) { $.ajax({ url: url, - dataType: 'xml', success: function (data) { - const layoutJson = data.querySelector('[id="bodyContent"]') - .querySelector('[class="mw-parser-output"]') - .children[0] - .firstChild.textContent; + // Hacky McHackFace has been working here. This probably break in the future + const startTrigger = "
"; + const start = data.indexOf(startTrigger); + data = data.substr(start, + data.indexOf("
") - start) + data = data.substr(0, data.lastIndexOf("

")) + data = data.substr(startTrigger.length + 3); + try { - console.log("DOWNLOADED:",layoutJson); - const parsed = JSON.parse(layoutJson); + const parsed = JSON.parse(data); parsed["id"] = layoutFromBase64 - const layout =new LayoutConfig(parsed); - InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(layoutJson)); + const layout = new LayoutConfig(parsed); + InitUiElements.InitAll(layout, layoutFromBase64, testing, layoutFromBase64, btoa(data)); } catch (e) { new FixedUiElement(`${themeName} is invalid:
${e}`) .SetClass("clickable") @@ -118,3 +108,4 @@ if (layoutFromBase64.startsWith("wiki:")) { InitUiElements.InitAll(layoutToUse, layoutFromBase64, testing, defaultLayout); } +// console.log(QueryParameters.GenerateQueryParameterDocs()) diff --git a/install.bat b/install.bat deleted file mode 100644 index b66c116..0000000 --- a/install.bat +++ /dev/null @@ -1 +0,0 @@ -npm install \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index b175d16..21245de 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1774,9 +1774,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001066", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz", - "integrity": "sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw==" + "version": "1.0.30001157", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001157.tgz", + "integrity": "sha512-gOerH9Wz2IRZ2ZPdMfBvyOi3cjaz4O4dgNwPGzx8EhqAs4+2IL/O+fJsbt+znSigujoZG8bVcIAUM/I/E5K3MA==" }, "canvas": { "version": "2.6.1", diff --git a/package.json b/package.json index 712eac5..9de3284 100644 --- a/package.json +++ b/package.json @@ -16,16 +16,17 @@ "start": "parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", "test": "ts-node test/*", "generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json", - "generate:included-images": "ts-node generateIncludedImages.ts", - "generate:layouts": "ts-node createLayouts.ts", + "generate:images": "ts-node scripts/generateIncludedImages.ts", + "generate:translations": "ts-node scripts/generateTranslations.ts", + "generate:layouts": "ts-node scripts/createLayouts.ts", "optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'", - "generate": "npm run generate:included-images && npm run generate:layouts && npm run generate:editor-layer-index", - "build": "rm -rf dist/ npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", + "generate": "npm run generate:images && npm run generate:translations && npm run generate:layouts && npm run generate:editor-layer-index", + "build": "rm -rf dist/ && npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*", "prepare-deploy": "npm run generate && npm run build && rm -rf .cache", "deploy:staging": "npm run prepare-deploy && rm -rf /home/pietervdvn/git/pietervdvn.github.io/Staging/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/Staging/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean", "deploy:production": "npm run prepare-deploy && npm run optimize-images && rm -rf /home/pietervdvn/git/pietervdvn.github.io/MapComplete/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/MapComplete/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean", - "clean": "rm *.webmanifest && find *.html | grep -v \"\\(index\\|land\\|test\\|preferences\\|customGenerator\\).html\" | xargs rm" + "clean": "rm -rf .cache/ && (find *.html | grep -v \"\\(index\\|land\\|test\\|preferences\\|customGenerator\\).html\" | xargs rm) && (find *.webmanifest | xargs rm)" }, "keywords": [ diff --git a/createLayouts.ts b/scripts/createLayouts.ts similarity index 92% rename from createLayouts.ts rename to scripts/createLayouts.ts index 6bd9802..86dfd85 100644 --- a/createLayouts.ts +++ b/scripts/createLayouts.ts @@ -1,16 +1,16 @@ -import {Img} from "./UI/Img" -import {UIElement} from "./UI/UIElement"; +import {Img} from "../UI/Img" +import {UIElement} from "../UI/UIElement"; Img.runningFromConsole = true; // We HAVE to mark this while importing UIElement.runningFromConsole = true; -import {AllKnownLayouts} from "./Customizations/AllKnownLayouts"; +import {AllKnownLayouts} from "../Customizations/AllKnownLayouts"; import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs"; -import Locale from "./UI/i18n/Locale"; +import Locale from "../UI/i18n/Locale"; import svg2img from 'promise-svg2img'; -import Translations from "./UI/i18n/Translations"; -import {Translation} from "./UI/i18n/Translation"; -import LayoutConfig from "./Customizations/JSON/LayoutConfig"; +import Translations from "../UI/i18n/Translations"; +import {Translation} from "../UI/i18n/Translation"; +import LayoutConfig from "../Customizations/JSON/LayoutConfig"; function enc(str: string): string { @@ -197,12 +197,18 @@ function createLandingPage(layout: LayoutConfig) { Locale.language.setData(layout.language[0]); const ogTitle = Translations.W(layout.title)?.InnerRender(); - const ogDescr = Translations.W(layout.description ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRender(); + const ogDescr = Translations.W(layout.shortDescription ?? "Easily add and edit geodata with OpenStreetMap")?.InnerRender(); const ogImage = layout.socialImage; let customCss = ""; if (layout.customCss !== undefined && layout.customCss !== "") { - customCss = `` + } } const og = ` @@ -237,6 +243,11 @@ function createLandingPage(layout: LayoutConfig) { return output; } +const generatedDir = "./assets/generated"; +if (! existsSync(generatedDir)) { + mkdirSync(generatedDir) +} + const blacklist = ["", "test", ".", "..", "manifest", "index", "land", "preferences", "account", "openstreetmap"] const all = AllKnownLayouts.allSets; @@ -245,10 +256,7 @@ let wikiPage = "{|class=\"wikitable sortable\"\n" + "|-"; -const generatedDir = "./assets/generated"; -if (! existsSync(generatedDir)) { - mkdirSync(generatedDir) -} + for (const layoutName in all) { if (blacklist.indexOf(layoutName.toLowerCase()) >= 0) { diff --git a/generateIncludedImages.ts b/scripts/generateIncludedImages.ts similarity index 51% rename from generateIncludedImages.ts rename to scripts/generateIncludedImages.ts index 1237403..75261b2 100644 --- a/generateIncludedImages.ts +++ b/scripts/generateIncludedImages.ts @@ -1,5 +1,5 @@ import * as fs from "fs"; -import {Utils} from "./Utils"; +import {Utils} from "../Utils"; function genImages() { @@ -7,6 +7,7 @@ function genImages() { const dir = fs.readdirSync("./assets/svg") let module = "import {Img} from \"./UI/Img\";\nimport {FixedUiElement} from \"./UI/Base/FixedUiElement\";\n\nexport default class Svg {\n\n\n"; + const allNames: string[] = []; for (const path of dir) { if (!path.endsWith(".svg")) { @@ -14,6 +15,8 @@ function genImages() { } const svg = fs.readFileSync("./assets/svg/" + path, "utf-8") + .replace(/<\?xml.*?>/, "") + .replace(/fill: ?none;/g,"fill: none !important;") // This is such a brittle hack... .replace(/\n/g, " ") .replace(/\r/g, "") .replace(/\\/g, "\\") @@ -22,48 +25,13 @@ function genImages() { .replace(/[ -]/g, "_"); module += ` public static ${name} = "${svg}"\n` module += ` public static ${name}_img = Img.AsImageElement(Svg.${name})\n` + module += ` public static ${name}_svg() { return new FixedUiElement(Svg.${name});}\n` module += ` public static ${name}_ui() { return new FixedUiElement(Svg.${name}_img);}\n\n` + allNames.push(`"${path}": Svg.${name}`) } + module += `public static All = {${allNames.join(",")}};` module += "}\n"; fs.writeFileSync("Svg.ts", module); console.log("Done") } - -function isTranslation(tr: any): boolean { - for (const key in tr) { - if (typeof tr[key] !== "string") { - return false; - } - } - return true; -} - -function transformTranslation(obj: any, depth = 1) { - - if (isTranslation(obj)) { - return `new Translation( ${JSON.stringify(obj)} )` - } - - let values = "" - for (const key in obj) { - values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n" - } - return `{${values}}`; - -} - -function genTranslations() { - const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8")) - const transformed = transformTranslation(translations); - - let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`; - module += " public static t = " + transformed; - module += "}" - - fs.writeFileSync("AllTranslationAssets.ts", module); - - -} - -genTranslations() genImages() \ No newline at end of file diff --git a/scripts/generateTranslations.ts b/scripts/generateTranslations.ts new file mode 100644 index 0000000..590cd08 --- /dev/null +++ b/scripts/generateTranslations.ts @@ -0,0 +1,40 @@ +import * as fs from "fs"; +import {Utils} from "../Utils"; + +function isTranslation(tr: any): boolean { + for (const key in tr) { + if (typeof tr[key] !== "string") { + return false; + } + } + return true; +} + +function transformTranslation(obj: any, depth = 1) { + + if (isTranslation(obj)) { + return `new Translation( ${JSON.stringify(obj)} )` + } + + let values = "" + for (const key in obj) { + values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n" + } + return `{${values}}`; + +} + +function genTranslations() { + const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8")) + const transformed = transformTranslation(translations); + + let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`; + module += " public static t = " + transformed; + module += "}" + + fs.writeFileSync("AllTranslationAssets.ts", module); + + +} + +genTranslations() \ No newline at end of file diff --git a/test.html b/test.html index fe0a777..521de22 100644 --- a/test.html +++ b/test.html @@ -25,7 +25,10 @@ -
'maindiv' not attached
+
+ +
'maindiv' not attached
+
'extradiv' not attached
diff --git a/test.ts b/test.ts index 77feaa5..1a50ef3 100644 --- a/test.ts +++ b/test.ts @@ -1,20 +1,20 @@ -/* +//* +import Direction from "./UI/Input/DirectionInput"; import {UIEventSource} from "./Logic/UIEventSource"; -import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox"; -import SharedLayers from "./Customizations/SharedLayers"; +import {VariableUiElement} from "./UI/Base/VariableUIElement"; -const tags = { - mapillary: "wweALGY5g8_T8UjGkcWCfw", - wikimedia_commons: "File:Boekenkast Sint-Lodewijks.jpg" -} -const src = new UIEventSource(tags); +const d = new UIEventSource("90"); +new Direction(d, [51.21576,3.22001]).AttachTo("maindiv") +new VariableUiElement(d.map(d => "" + d + "°")).AttachTo("extradiv") -new FeatureInfoBox(src, SharedLayers.sharedLayers["ghost_bike"]).AttachTo('maindiv'); +UIEventSource.Chronic(25, () => { + const degr = (Number(d.data) + 1) % 360; + d.setData(""+ degr); + return true; +}) -//const subs = new SubstitutedTranslation(new Translation({"nl":"NL {image_carousel()} {image_upload()}"}), src) -//subs.AttachTo("maindiv") /*/