forked from MapComplete/MapComplete
		
	I should have commited sooner...
This commit is contained in:
		
							parent
							
								
									2685b6e734
								
							
						
					
					
						commit
						16612b10ef
					
				
					 35 changed files with 570 additions and 177 deletions
				
			
		|  | @ -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 {Browser} from "leaflet"; | ||||
| 
 | ||||
| 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; | ||||
|  | @ -53,10 +63,11 @@ export default class LayerConfig { | |||
|         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), | ||||
|  | @ -93,7 +104,10 @@ export default class LayerConfig { | |||
| 
 | ||||
|         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,11 +121,19 @@ 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", ""); | ||||
| 
 | ||||
| 
 | ||||
|  | @ -121,13 +143,16 @@ export default class LayerConfig { | |||
|     public GenerateLeafletStyle(tags: any): | ||||
|         { | ||||
|             color: string; | ||||
|             icon: { popupAnchor: [number, number]; iconAnchor: [number, number]; iconSize: [number, number]; iconUrl: string }; weight: number; dashArray: number[] | ||||
|             icon: { | ||||
|                 iconUrl: string, | ||||
|                 popupAnchor: [number, number]; | ||||
|                 iconAnchor: [number, number]; | ||||
|                 iconSize: [number, number]; | ||||
|                 html: string; | ||||
|                 rotation: number; | ||||
|             }; | ||||
|             weight: number; dashArray: number[] | ||||
|         } { | ||||
|         const iconUrl = this.icon?.GetRenderValue(tags)?.txt; | ||||
|         const iconSize = (this.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(","); | ||||
| 
 | ||||
| 
 | ||||
|         const dashArray = this.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number); | ||||
| 
 | ||||
|         function num(str, deflt = 40) { | ||||
|             const n = Number(str); | ||||
|  | @ -137,6 +162,33 @@ export default class LayerConfig { | |||
|             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 = rendernum(this.rotation, 0); | ||||
| 
 | ||||
| 
 | ||||
|         const iconW = num(iconSize[0]); | ||||
|         const iconH = num(iconSize[1]); | ||||
|         const mode = iconSize[2] ?? "center" | ||||
|  | @ -157,16 +209,22 @@ export default class LayerConfig { | |||
|             anchorH = iconH; | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         const color = this.color?.GetRenderValue(tags)?.txt ?? "#00f"; | ||||
|         let weight = num(this.width?.GetRenderValue(tags)?.txt, 5); | ||||
|         let html = `<img src="${iconUrl}" style="width:100%;height:100%;rotate:${rotation}deg;display:block;" />`; | ||||
|         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}deg;display:block;`).Render(); | ||||
|         } | ||||
|         return { | ||||
|             icon: | ||||
|                 { | ||||
|                     iconUrl: iconUrl, | ||||
|                     html: html, | ||||
|                     iconSize: [iconW, iconH], | ||||
|                     iconAnchor: [anchorW, anchorH], | ||||
|                     popupAnchor: [0, 3 - anchorH] | ||||
|                     popupAnchor: [0, 3 - anchorH], | ||||
|                     rotation: rotation, | ||||
|                     iconUrl: iconUrl | ||||
|                 }, | ||||
|             color: color, | ||||
|             weight: weight, | ||||
|  |  | |||
|  | @ -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,14 @@ export interface LayerConfigJson { | |||
|      * Default is '40,40,center' | ||||
|      */ | ||||
|     iconSize?: string | TagRenderingConfigJson; | ||||
|     /** | ||||
|      * The rotation of an icon, useful for e.g. directions | ||||
|      */ | ||||
|     rotation?: string | TagRenderingConfigJson; | ||||
| 
 | ||||
|     /** | ||||
|      * The color for way-elements | ||||
|      * 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 +103,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 +119,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) [] | ||||
| } | ||||
|  | @ -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)[], | ||||
| 
 | ||||
|  |  | |||
|  | @ -13,6 +13,7 @@ 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 LayerConfig from "./JSON/LayerConfig"; | ||||
| 
 | ||||
| export default class SharedLayers { | ||||
|  | @ -37,6 +38,7 @@ export default class SharedLayers { | |||
|             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")  | ||||
|         ]; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 { | ||||
| 
 | ||||
|  | @ -142,6 +145,7 @@ export class InitUiElements { | |||
| 
 | ||||
|         } | ||||
| 
 | ||||
| 
 | ||||
|         if (layoutToUse.id === personal.id) { | ||||
|             State.state.favouriteLayers.addCallback(updateFavs); | ||||
|             State.state.installedThemes.addCallback(updateFavs); | ||||
|  | @ -153,6 +157,10 @@ export class InitUiElements { | |||
|          * This is given to the div which renders fullscreen on mobile devices | ||||
|          */ | ||||
|         State.state.selectedElement.addCallback((feature) => { | ||||
| 
 | ||||
|                 if (feature === undefined) { | ||||
|                     State.state.fullScreenMessage.setData(undefined); | ||||
|                 } | ||||
|                 if (feature?.properties === undefined) { | ||||
|                     return; | ||||
|                 } | ||||
|  | @ -163,9 +171,15 @@ export class InitUiElements { | |||
|                         continue; | ||||
|                     } | ||||
|                     const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data)); | ||||
|                     if (applicable) { | ||||
|                         // This layer is the layer that gives the questions
 | ||||
|                     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 | ||||
|  | @ -175,7 +189,6 @@ export class InitUiElements { | |||
|                     break; | ||||
|                 } | ||||
|             } | ||||
|             } | ||||
|         ); | ||||
| 
 | ||||
|         InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => { | ||||
|  | @ -204,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;`) | ||||
|  | @ -327,6 +355,10 @@ export class InitUiElements { | |||
|             checkbox.isEnabled.setData(false); | ||||
|         }) | ||||
| 
 | ||||
|         State.state.selectedElement.addCallback(() => { | ||||
|             checkbox.isEnabled.setData(false); | ||||
|         }) | ||||
| 
 | ||||
| 
 | ||||
|         const fullOptions2 = this.CreateWelcomePane(); | ||||
|         State.state.fullScreenMessage.setData(fullOptions2) | ||||
|  | @ -435,13 +467,15 @@ 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; | ||||
|  | @ -475,13 +509,12 @@ export class InitUiElements { | |||
|                 throw "Layer " + layer + " was not substituted"; | ||||
|             } | ||||
| 
 | ||||
|             const flayer: FilteredLayer = new FilteredLayer(layer, | ||||
|                 (tagsES) => { | ||||
|                     return new FeatureInfoBox( | ||||
|                         tagsES, | ||||
|                         layer, | ||||
|                     ) | ||||
|                 }); | ||||
|             let generateContents = (tags: UIEventSource<any>) => new FeatureInfoBox(tags, layer); | ||||
|             if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) { | ||||
|                 generateContents = undefined; | ||||
|             } | ||||
| 
 | ||||
|             const flayer: FilteredLayer = new FilteredLayer(layer, generateContents); | ||||
|             flayers.push(flayer); | ||||
| 
 | ||||
|             QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown") | ||||
|  |  | |||
|  | @ -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 | ||||
|  | @ -75,12 +76,14 @@ export class FilteredLayer { | |||
|         const selfFeatures = []; | ||||
|         for (let feature of geojson.features) { | ||||
|             const tags = TagUtils.proprtiesToKV(feature.properties); | ||||
|             if (!this.filters.matches(tags)) { | ||||
|                 leftoverFeatures.push(feature); | ||||
|                 continue; | ||||
|             } | ||||
|             const matches = this.filters.matches(tags); | ||||
|             if (matches) { | ||||
|                 selfFeatures.push(feature); | ||||
|             } | ||||
|             if (!matches || this.layerDef.passAllFeatures) { | ||||
|                 leftoverFeatures.push(feature); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|        this.RenderLayer(selfFeatures) | ||||
| 
 | ||||
|  | @ -117,7 +120,6 @@ export class FilteredLayer { | |||
| 
 | ||||
|         // We fetch all the data we have to show:
 | ||||
|         let fusedFeatures = this.ApplyWayHandling(this.FuseData(features)); | ||||
|         console.log("Fused:",fusedFeatures) | ||||
| 
 | ||||
|         // And we copy some features as points - if needed
 | ||||
|         const data = { | ||||
|  | @ -126,7 +128,6 @@ export class FilteredLayer { | |||
|         } | ||||
| 
 | ||||
|         let self = this; | ||||
|         console.log(data); | ||||
|         this._geolayer = L.geoJSON(data, { | ||||
|                 style: feature => | ||||
|                     self.layerDef.GenerateLeafletStyle(feature.properties), | ||||
|  | @ -147,19 +148,21 @@ export class FilteredLayer { | |||
|                             color: style.color | ||||
|                         }); | ||||
|                     } else { | ||||
|                         if (style.icon.iconSize === undefined) { | ||||
|                             style.icon.iconSize = [50, 50] | ||||
|                         } | ||||
| 
 | ||||
|                         marker = L.marker(latLng, { | ||||
|                             icon: L.icon(style.icon) | ||||
|                             icon: L.divIcon(style.icon) | ||||
|                         }); | ||||
|                     } | ||||
|                     return marker; | ||||
|                 }, | ||||
|                 onEachFeature: function (feature, layer: Layer) { | ||||
| 
 | ||||
|                     layer.on("click", (e) => { | ||||
|                     if (self._showOnPopup === undefined) { | ||||
|                         // No popup contents defined -> don't do anything
 | ||||
|                         return; | ||||
|                     } | ||||
| 
 | ||||
| 
 | ||||
|                     function openPopup(latlng: any) { | ||||
|                         if (layer.getPopup() === undefined | ||||
|                             && (window.screen.availHeight > 600 || window.screen.availWidth > 600) // We DON'T trigger this code on small screens! No need to create a popup
 | ||||
|                         ) { | ||||
|  | @ -168,8 +171,7 @@ export class FilteredLayer { | |||
|                                 closeOnEscapeKey: true, | ||||
|                             }, layer); | ||||
| 
 | ||||
|                         // @ts-ignore
 | ||||
|                         popup.setLatLng(e.latlng) | ||||
|                             popup.setLatLng(latlng) | ||||
| 
 | ||||
|                             layer.bindPopup(popup); | ||||
|                             const eventSource = State.state.allElements.addOrGetElement(feature); | ||||
|  | @ -181,17 +183,35 @@ export class FilteredLayer { | |||
|                             // popup.openOn(State.state.bm.map);
 | ||||
|                             // ANd we perform the pending update
 | ||||
|                             uiElement.Update(); | ||||
|                             // @ts-ignore
 | ||||
|                             popup.Update = () => { | ||||
|                                 uiElement.Update(); | ||||
|                             } | ||||
|                         } else { | ||||
|                             // @ts-ignore
 | ||||
|                             layer.getPopup().Update(); | ||||
|                         } | ||||
| 
 | ||||
| 
 | ||||
|                         // We set the element as selected...
 | ||||
|                         State.state.selectedElement.setData(feature); | ||||
| 
 | ||||
|                     } | ||||
| 
 | ||||
|                     layer.on("click", (e) => { | ||||
|                         // @ts-ignore
 | ||||
|                         openPopup(e.latlng); | ||||
|                         // We mark the event as consumed
 | ||||
|                         L.DomEvent.stop(e); | ||||
|                     }); | ||||
| 
 | ||||
|                     if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) { | ||||
|                         const center = GeoOperations.centerpoint(feature).geometry.coordinates; | ||||
|                         openPopup({lat: center[1], lng: center[0]}) | ||||
|                     } | ||||
| 
 | ||||
|                 } | ||||
|         ) | ||||
|         ; | ||||
|         }); | ||||
| 
 | ||||
|         if (this.combinedIsDisplayed.data) { | ||||
|             this._geolayer.addTo(State.state.bm.map); | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
| 
 | ||||
|         State.state.currentGPSLocation.addCallback((location) => { | ||||
| 
 | ||||
|             const color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color") | ||||
|             const icon = L.icon( | ||||
|                 { | ||||
|                 iconUrl: Img.AsData(Svg.crosshair_blue), | ||||
|                     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
 | ||||
|                 }) | ||||
|              | ||||
|         State.state.currentGPSLocation.addCallback((location) => { | ||||
|             const newMarker = L.marker(location.latlng, {icon: icon}); | ||||
|             newMarker.addTo(map); | ||||
| 
 | ||||
|  |  | |||
|  | @ -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++) { | ||||
|  |  | |||
							
								
								
									
										18
									
								
								Logic/Web/Hash.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								Logic/Web/Hash.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,18 @@ | |||
| import {UIEventSource} from "../UIEventSource"; | ||||
| 
 | ||||
| export default class Hash { | ||||
|      | ||||
|     public static Get() : UIEventSource<string>{ | ||||
|         const hash = new UIEventSource<string>(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; | ||||
|     } | ||||
|      | ||||
| } | ||||
|  | @ -2,6 +2,7 @@ | |||
|  * Wraps the query parameters into UIEventSources | ||||
|  */ | ||||
| import {UIEventSource} from "../UIEventSource"; | ||||
| import Hash from "./Hash"; | ||||
| 
 | ||||
| export class QueryParameters { | ||||
| 
 | ||||
|  | @ -57,7 +58,7 @@ 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); | ||||
| 
 | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										19
									
								
								State.ts
									
										
									
									
									
								
							
							
						
						
									
										19
									
								
								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 | ||||
|  | @ -22,7 +23,7 @@ export default class State { | |||
|     // The singleton of the global state
 | ||||
|     public static state: State; | ||||
| 
 | ||||
|     public static vNumber = "0.1.3-rc2+g"; | ||||
|     public static vNumber = "0.1.3-rc4"; | ||||
| 
 | ||||
|     // The user journey states thresholds when a new feature gets unlocked
 | ||||
|     public static userJourney = { | ||||
|  | @ -209,6 +210,22 @@ export default class State { | |||
|         ); | ||||
| 
 | ||||
| 
 | ||||
|         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) { | ||||
|  |  | |||
							
								
								
									
										7
									
								
								Svg.ts
									
										
									
									
									
								
							
							
						
						
									
										7
									
								
								Svg.ts
									
										
									
									
									
								
							|  | @ -79,6 +79,11 @@ export default class Svg { | |||
|     public static direction_svg() { return new FixedUiElement(Svg.direction);} | ||||
|     public static direction_ui() { return new FixedUiElement(Svg.direction_img);} | ||||
| 
 | ||||
|     public static direction_gradient = " <svg    xmlns:dc=\"http://purl.org/dc/elements/1.1/\"    xmlns:cc=\"http://creativecommons.org/ns#\"    xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"    xmlns:svg=\"http://www.w3.org/2000/svg\"    xmlns=\"http://www.w3.org/2000/svg\"    xmlns:xlink=\"http://www.w3.org/1999/xlink\"    width=\"100\"    height=\"100\"    viewBox=\"0 0 100 100\"    version=\"1.1\"    id=\"svg8\">   <metadata      id=\"metadata8\">     <rdf:RDF>       <cc:Work          rdf:about=\"\">         <dc:format>image/svg+xml</dc:format>         <dc:type            rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />         <dc:title></dc:title>       </cc:Work>     </rdf:RDF>   </metadata>   <defs      id=\"defs6\">     <linearGradient        id=\"linearGradient820\">       <stop          id=\"stop816\"          offset=\"0\"          style=\"stop-color:#000000;stop-opacity:1;\" />       <stop          id=\"stop818\"          offset=\"1\"          style=\"stop-color:#000000;stop-opacity:0\" />     </linearGradient>     <radialGradient        gradientUnits=\"userSpaceOnUse\"        gradientTransform=\"matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)\"        r=\"28.883806\"        fy=\"53.828533\"        fx=\"49.787739\"        cy=\"53.828533\"        cx=\"49.787739\"        id=\"radialGradient828\"        xlink:href=\"#linearGradient820\" />   </defs>   <path      style=\"fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\"      d=\"M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z\"      id=\"path821\" /> </svg> " | ||||
|     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 = " <svg    xmlns:dc=\"http://purl.org/dc/elements/1.1/\"    xmlns:cc=\"http://creativecommons.org/ns#\"    xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"    xmlns:svg=\"http://www.w3.org/2000/svg\"    xmlns=\"http://www.w3.org/2000/svg\"    xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\"    xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\"    version=\"1.0\"    width=\"700\"    height=\"700\"    id=\"svg6\"    sodipodi:docname=\"down.svg\"    inkscape:version=\"0.92.4 (5da689c313, 2019-01-14)\">   <metadata      id=\"metadata12\">     <rdf:RDF>       <cc:Work          rdf:about=\"\">         <dc:format>image/svg+xml</dc:format>         <dc:type            rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" />       </cc:Work>     </rdf:RDF>   </metadata>   <defs      id=\"defs10\" />   <sodipodi:namedview      pagecolor=\"#ffffff\"      bordercolor=\"#666666\"      borderopacity=\"1\"      objecttolerance=\"10\"      gridtolerance=\"10\"      guidetolerance=\"10\"      inkscape:pageopacity=\"0\"      inkscape:pageshadow=\"2\"      inkscape:window-width=\"1920\"      inkscape:window-height=\"1001\"      id=\"namedview8\"      showgrid=\"false\"      inkscape:zoom=\"0.33714286\"      inkscape:cx=\"477.91309\"      inkscape:cy=\"350\"      inkscape:window-x=\"0\"      inkscape:window-y=\"0\"      inkscape:window-maximized=\"1\"      inkscape:current-layer=\"svg6\" />   <g      transform=\"rotate(-180,342.1439,335.17672)\"      id=\"g4\">     <path        d=\"m -20,670.71582 c 0,-1.85843 349.99229,-699.98853 350.57213,-699.28671 1.94549,2.35478 350.06752,699.46087 349.427,699.71927 -0.41837,0.16878 -79.29725,-33.69092 -175.2864,-75.24377 l -174.52574,-75.55065 -174.2421,75.53732 c -95.83317,41.54551 -174.625237,75.5373 -175.093498,75.5373 -0.46826,0 -0.851382,-0.32075 -0.851382,-0.71276 z\"        style=\"fill:#00ff00;stroke:none\"        id=\"path2\"        inkscape:connector-curvature=\"0\" />   </g> </svg> " | ||||
|     public static down_img = Img.AsImageElement(Svg.down) | ||||
|     public static down_svg() { return new FixedUiElement(Svg.down);} | ||||
|  | @ -214,4 +219,4 @@ export default class Svg { | |||
|     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};} | ||||
|  |  | |||
|  | @ -14,7 +14,6 @@ export class FullScreenMessageBox extends UIElement { | |||
|     constructor(onClear: (() => void)) { | ||||
|         super(State.state.fullScreenMessage); | ||||
|         this.HideOnEmpty(true); | ||||
|         const self = this; | ||||
| 
 | ||||
|         this.returnToTheMap = | ||||
|             new Combine([Translations.t.general.returnToTheMap.Clone().SetStyle("font-size:xx-large")]) | ||||
|  |  | |||
|  | @ -2,6 +2,10 @@ 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 | ||||
|  | @ -34,8 +38,8 @@ export default class DirectionInput extends InputElement<string> { | |||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|         console.log("Inner render direction") | ||||
|         return new Combine([ | ||||
|             `<div id="direction-leaflet-div-${this.id}" style="width:100%;height: 100%;position: absolute;top:0;left:0;border-radius:100%;"></div>`, | ||||
|             Svg.direction_svg().SetStyle( | ||||
|                 `position: absolute;top: 0;left: 0;width: 100%;height: 100%;rotate:${this.value.data}deg;`) | ||||
|                 .SetClass("direction-svg"), | ||||
|  | @ -47,7 +51,6 @@ export default class DirectionInput extends InputElement<string> { | |||
|     } | ||||
| 
 | ||||
|     protected InnerUpdate(htmlElement: HTMLElement) { | ||||
|          console.log("Inner update direction") | ||||
|         super.InnerUpdate(htmlElement); | ||||
|         const self = this; | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,9 @@ interface TextFieldDef { | |||
|     explanation: string, | ||||
|     isValid: ((s: string, country?: string) => boolean), | ||||
|     reformat?: ((s: string, country?: string) => string), | ||||
|     inputHelper?: (value: UIEventSource<string>) => InputElement<string>, | ||||
|     inputHelper?: (value: UIEventSource<string>, options?: { | ||||
|         location: [number, number] | ||||
|     }) => InputElement<string>, | ||||
| } | ||||
| 
 | ||||
| export default class ValidatedTextField { | ||||
|  | @ -26,7 +28,9 @@ export default class ValidatedTextField { | |||
|                       explanation: string, | ||||
|                       isValid?: ((s: string, country?: string) => boolean), | ||||
|                       reformat?: ((s: string, country?: string) => string), | ||||
|                       inputHelper?: (value: UIEventSource<string>) => InputElement<string>): TextFieldDef { | ||||
|                       inputHelper?: (value: UIEventSource<string>, options?:{ | ||||
|                           location: [number, number] | ||||
|                       }) => InputElement<string>): TextFieldDef { | ||||
| 
 | ||||
|         if (isValid === undefined) { | ||||
|             isValid = () => true; | ||||
|  | @ -197,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<string> { | ||||
|         options = options ?? {}; | ||||
|         options.placeholder = options.placeholder ?? type; | ||||
|  | @ -230,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; | ||||
|     } | ||||
|  |  | |||
|  | @ -28,7 +28,8 @@ export class FeatureInfoBox extends UIElement { | |||
|         this._layerConfig = layerConfig; | ||||
| 
 | ||||
| 
 | ||||
|         this._title = new TagRenderingAnswer(tags, layerConfig.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))) | ||||
|  |  | |||
|  | @ -15,6 +15,9 @@ export default class TagRenderingAnswer extends UIElement { | |||
|         super(tags); | ||||
|         this._tags = tags; | ||||
|         this._configuration = configuration; | ||||
|         if(configuration === undefined){ | ||||
|             throw "Trying to generate a tagRenderingAnswer without configuration..." | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     InnerRender(): string { | ||||
|  |  | |||
|  | @ -251,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]); | ||||
|  |  | |||
|  | @ -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[] { | ||||
|  |  | |||
|  | @ -94,15 +94,6 @@ export class UserBadge extends UIElement { | |||
|             dryrun = new FixedUiElement("TESTING").SetClass("alert"); | ||||
|         } | ||||
| 
 | ||||
|         if (user.home !== undefined) { | ||||
|             const icon = L.icon({ | ||||
|                 iconUrl: Img.AsData(Svg.home_white_bg), | ||||
|                 iconSize: [30, 30], | ||||
|                 iconAnchor: [15, 15] | ||||
|             }); | ||||
|             L.marker([user.home.lat, user.home.lon], {icon: icon}).addTo(State.state.bm.map) | ||||
|         } | ||||
| 
 | ||||
|         const settings = | ||||
|             new Link(Svg.gear_svg(), | ||||
|                 `https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,  | ||||
|  |  | |||
							
								
								
									
										3
									
								
								Utils.ts
									
										
									
									
									
								
							
							
						
						
									
										3
									
								
								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, '&') | ||||
|  | @ -167,4 +168,6 @@ export class Utils { | |||
|         console.log("Added custom layout ",location) | ||||
|     } | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| } | ||||
|  |  | |||
							
								
								
									
										32
									
								
								assets/layers/direction/direction.json
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								assets/layers/direction/direction.json
									
										
									
									
									
										Normal file
									
								
							|  | @ -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}", | ||||
|     "mappings": [ | ||||
|       { | ||||
|         "if": "direction~*", | ||||
|         "then": "{direction}" | ||||
|       } | ||||
|     ] | ||||
|   }, | ||||
|   "iconSize":  "200,200,center", | ||||
|   "color": "--catch-detail-color", | ||||
|   "stroke": "0", | ||||
|   "presets": [], | ||||
|   "wayHandling": 2 | ||||
| } | ||||
							
								
								
									
										54
									
								
								assets/svg/direction_gradient.svg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								assets/svg/direction_gradient.svg
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,54 @@ | |||
| <?xml version="1.0" encoding="UTF-8" standalone="no"?> | ||||
| <svg | ||||
|    xmlns:dc="http://purl.org/dc/elements/1.1/" | ||||
|    xmlns:cc="http://creativecommons.org/ns#" | ||||
|    xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" | ||||
|    xmlns:svg="http://www.w3.org/2000/svg" | ||||
|    xmlns="http://www.w3.org/2000/svg" | ||||
|    xmlns:xlink="http://www.w3.org/1999/xlink" | ||||
|    width="100" | ||||
|    height="100" | ||||
|    viewBox="0 0 100 100" | ||||
|    version="1.1" | ||||
|    id="svg8"> | ||||
|   <metadata | ||||
|      id="metadata8"> | ||||
|     <rdf:RDF> | ||||
|       <cc:Work | ||||
|          rdf:about=""> | ||||
|         <dc:format>image/svg+xml</dc:format> | ||||
|         <dc:type | ||||
|            rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> | ||||
|         <dc:title></dc:title> | ||||
|       </cc:Work> | ||||
|     </rdf:RDF> | ||||
|   </metadata> | ||||
|   <defs | ||||
|      id="defs6"> | ||||
|     <linearGradient | ||||
|        id="linearGradient820"> | ||||
|       <stop | ||||
|          id="innercolor" | ||||
|          offset="0" | ||||
|          style="stop-color:#000000;stop-opacity:1;" /> | ||||
|       <stop | ||||
|          id="outercolor" | ||||
|          offset="1" | ||||
|          style="stop-color:#000000;stop-opacity:0" /> | ||||
|     </linearGradient> | ||||
|     <radialGradient | ||||
|        gradientUnits="userSpaceOnUse" | ||||
|        gradientTransform="matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)" | ||||
|        r="28.883806" | ||||
|        fy="53.828533" | ||||
|        fx="49.787739" | ||||
|        cy="53.828533" | ||||
|        cx="49.787739" | ||||
|        id="radialGradient828" | ||||
|        xlink:href="#linearGradient820" /> | ||||
|   </defs> | ||||
|   <path | ||||
|      style="fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" | ||||
|      d="M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z" | ||||
|      id="path821" /> | ||||
| </svg> | ||||
| After Width: | Height: | Size: 1.7 KiB | 
|  | @ -10,3 +10,17 @@ html { | |||
|     --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; | ||||
| } | ||||
| */ | ||||
|  |  | |||
|  | @ -27,6 +27,7 @@ | |||
|   "customCss": "./assets/themes/surveillance_cameras/custom_theme.css", | ||||
|   "defaultBackgroundId": "Stadia.AlidadeSmoothDark", | ||||
|   "layers": [ | ||||
|     "direction", | ||||
|     { | ||||
|       "id": "cameras", | ||||
|       "name": { | ||||
|  | @ -56,6 +57,7 @@ | |||
|       "tagRenderings": [ | ||||
|         "images", | ||||
|         { | ||||
|           "#": "Camera type: fixed; panning; dome", | ||||
|           "question": { | ||||
|             "en": "What kind of camera is this?", | ||||
|             "nl": "Wat voor soort camera is dit?" | ||||
|  | @ -97,18 +99,32 @@ | |||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "#": "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": "camera:type!=dome", | ||||
|           "condition": { | ||||
|             "not": { | ||||
|               "and": [ | ||||
|                 "camera:type=dome", | ||||
|                 { | ||||
|                   "or": [ | ||||
|                     "camera:mount=ceiling", | ||||
|                     "camera:mount=pole" | ||||
|                   ] | ||||
|                 } | ||||
|               ] | ||||
|             } | ||||
|           }, | ||||
|           "freeform": { | ||||
|             "key": "camera:direction", | ||||
|             "type": "direction" | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "#": "Operator", | ||||
|           "freeform": { | ||||
|             "key": "operator" | ||||
|           }, | ||||
|  | @ -122,6 +138,7 @@ | |||
|           } | ||||
|         }, | ||||
|         { | ||||
|           "#": "Surveillance type: public, outdoor, indoor", | ||||
|           "question": { | ||||
|             "en": "What kind of surveillance is this camera", | ||||
|             "nl": "Wat soort bewaking wordt hier uitgevoerd?" | ||||
|  | @ -134,8 +151,8 @@ | |||
|                 ] | ||||
|               }, | ||||
|               "then": { | ||||
|                 "en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station...", | ||||
|                 "nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw..." | ||||
|                 "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..." | ||||
|               } | ||||
|             }, | ||||
|             { | ||||
|  | @ -156,13 +173,67 @@ | |||
|                 ] | ||||
|               }, | ||||
|               "then": { | ||||
|                 "nl": "Een private binnenruimte wordt bewaakt, bv. een wiinkel, een parkeergarage, ...", | ||||
|                 "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?" | ||||
|  | @ -244,6 +315,7 @@ | |||
|           ] | ||||
|         }, | ||||
|         { | ||||
|           "#": "camera:mount", | ||||
|           "question": { | ||||
|             "en": "How is this camera placed?", | ||||
|             "nl": "Hoe is deze camera geplaatst?" | ||||
|  | @ -267,10 +339,10 @@ | |||
|               } | ||||
|             }, | ||||
|             { | ||||
|               "if": "camera:mount=pole", | ||||
|               "if": "camera:mount=ceiling", | ||||
|               "then": { | ||||
|                 "en": "This camera is placed one a pole", | ||||
|                 "nl": "Deze camera staat op een paal" | ||||
|                 "en": "This camera is placed on the ceiling", | ||||
|                 "nl": "Deze camera hangt aan het plafond" | ||||
|               } | ||||
|             } | ||||
|           ] | ||||
|  | @ -293,7 +365,7 @@ | |||
|         "render": "50,50,center" | ||||
|       }, | ||||
|       "color": { | ||||
|         "render": "#00f" | ||||
|         "render": "#f00" | ||||
|       }, | ||||
|       "presets": [ | ||||
|         { | ||||
|  |  | |||
|  | @ -45,10 +45,6 @@ Contains tweaks for small screens | |||
|     } | ||||
|      | ||||
|     .leaflet-popup { | ||||
|         transform: unset !important; | ||||
|     } | ||||
| 
 | ||||
|     .leaflet-popup-content { | ||||
|         /* On mobile, the popups are shown as a full-screen element */ | ||||
|         display: none; | ||||
|         visibility: hidden; | ||||
|  |  | |||
|  | @ -38,6 +38,11 @@ | |||
| 
 | ||||
| } | ||||
| 
 | ||||
| .question svg { | ||||
|     width: 100%; | ||||
|     height: 100%; | ||||
| } | ||||
| 
 | ||||
| .question-text { | ||||
|     font-size: larger; | ||||
|     font-weight: bold; | ||||
|  |  | |||
|  | @ -255,7 +255,6 @@ a { | |||
|     position: absolute; | ||||
|     z-index: 5000; | ||||
|     transition: all 500ms linear; | ||||
|     overflow-x: hidden; | ||||
|     pointer-events: none; | ||||
|     /* Shadow offset */ | ||||
|     padding: 0.5em 10px 0 0.5em; | ||||
|  | @ -410,6 +409,12 @@ a { | |||
|     overflow-y: auto; | ||||
|     overflow-x: hidden; | ||||
| } | ||||
| 
 | ||||
| .leaflet-div-icon { | ||||
|     background-color: unset !important; | ||||
|     border: unset !important; | ||||
| } | ||||
| 
 | ||||
| /****** ShareScreen *****/ | ||||
| 
 | ||||
| .literal-code { | ||||
|  |  | |||
|  | @ -1 +0,0 @@ | |||
| npm install | ||||
|  | @ -16,10 +16,11 @@ | |||
|     "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: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:images && npm run generate:layouts && npm run generate:editor-layer-index", | ||||
|     "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", | ||||
|  |  | |||
|  | @ -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 { | ||||
|  | @ -100,7 +100,7 @@ const alreadyWritten = [] | |||
| 
 | ||||
| function createIcon(iconPath: string, size: number, layout: LayoutConfig) { | ||||
|     let name = iconPath.split(".").slice(0, -1).join("."); | ||||
|     if (name.startsWith("./")) { | ||||
|     if (name.startsWith("../")) { | ||||
|         name = name.substr(2) | ||||
|     } | ||||
|     const newname = `${name}${size}.png` | ||||
|  | @ -151,7 +151,7 @@ function createManifest(layout: LayoutConfig, relativePath: string) { | |||
|         let path = layout.icon; | ||||
|         if (layout.icon.startsWith("<")) { | ||||
|             // THis is already the svg
 | ||||
|             path = "./assets/generated/" + layout.id + "_logo.svg" | ||||
|             path = "../assets/generated/" + layout.id + "_logo.svg" | ||||
|             writeFileSync(path, layout.icon) | ||||
|         } | ||||
|          | ||||
|  | @ -212,19 +212,19 @@ function createLandingPage(layout: LayoutConfig) { | |||
|     } | ||||
| 
 | ||||
|     const og = ` | ||||
|     <meta property="og:image" content="${ogImage ?? './assets/svg/add.svg'}"> | ||||
|     <meta property="og:image" content="${ogImage ?? '../assets/svg/add.svg'}"> | ||||
|     <meta property="og:title" content="${ogTitle}"> | ||||
|     <meta property="og:description" content="${ogDescr}">` | ||||
| 
 | ||||
|     let icon = layout.icon; | ||||
|     if (icon.startsWith("<?xml") || icon.startsWith("<svg")) { | ||||
|         // This already is an svg
 | ||||
|         icon = `./assets/generated/${layout.id}_icon.svg` | ||||
|         icon = `../assets/generated/${layout.id}_icon.svg` | ||||
|         writeFileSync(icon, layout.icon); | ||||
|     } | ||||
| 
 | ||||
|     let output = template | ||||
|         .replace(`./manifest.manifest`, `./${enc(layout.id)}.webmanifest`) | ||||
|         .replace(`../manifest.manifest`, `../${enc(layout.id)}.webmanifest`) | ||||
|         .replace("<!-- $$$OG-META -->", og) | ||||
|         .replace(/<title>.+?<\/title>/, `<title>${ogTitle}</title>`) | ||||
|         .replace("Loading MapComplete, hang on...", `Loading MapComplete theme <i>${ogTitle}</i>...`) | ||||
|  | @ -251,7 +251,7 @@ let wikiPage = "{|class=\"wikitable sortable\"\n" + | |||
|     "|-"; | ||||
| 
 | ||||
| 
 | ||||
| const generatedDir = "./assets/generated"; | ||||
| const generatedDir = "../assets/generated"; | ||||
| if (! existsSync(generatedDir)) { | ||||
|     mkdirSync(generatedDir) | ||||
| } | ||||
|  | @ -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")) { | ||||
|  | @ -26,47 +27,11 @@ function genImages() { | |||
|         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() | ||||
							
								
								
									
										40
									
								
								scripts/generateTranslations.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								scripts/generateTranslations.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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() | ||||
|  | @ -25,7 +25,10 @@ | |||
|     </style> | ||||
| </head> | ||||
| <body> | ||||
| <div class="question"> | ||||
| 
 | ||||
|     <div id="maindiv">'maindiv' not attached</div> | ||||
| </div> | ||||
| <div id="extradiv">'extradiv' not attached</div> | ||||
| <script src="./test.ts"></script> | ||||
| </body> | ||||
|  |  | |||
							
								
								
									
										2
									
								
								test.ts
									
										
									
									
									
								
							
							
						
						
									
										2
									
								
								test.ts
									
										
									
									
									
								
							|  | @ -6,7 +6,7 @@ import {UIEventSource} from "./Logic/UIEventSource"; | |||
| import {VariableUiElement} from "./UI/Base/VariableUIElement"; | ||||
| 
 | ||||
| const d = new UIEventSource("90"); | ||||
| new Direction(d).AttachTo("maindiv") | ||||
| new Direction(d, [51.21576,3.22001]).AttachTo("maindiv") | ||||
| new VariableUiElement(d.map(d => "" + d + "°")).AttachTo("extradiv") | ||||
| 
 | ||||
| UIEventSource.Chronic(25, () => { | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue