More refactoring, stabilizing rotation and direction_gradient
This commit is contained in:
parent
5fec108ba2
commit
778044d0fb
45 changed files with 656 additions and 640 deletions
|
@ -11,7 +11,7 @@ export class FromJSON {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
|
public static Tag(json: AndOrTagConfigJson | string, context: string = ""): TagsFilter {
|
||||||
if(json === undefined){
|
if (json === undefined) {
|
||||||
throw `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
|
throw `Error while parsing a tag: 'json' is undefined in ${context}. Make sure all the tags are defined and at least one tag is present in a complex expression`
|
||||||
}
|
}
|
||||||
if (typeof (json) == "string") {
|
if (typeof (json) == "string") {
|
||||||
|
@ -58,12 +58,18 @@ export class FromJSON {
|
||||||
new RegExp("^" + split[1] + "$")
|
new RegExp("^" + split[1] + "$")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
if (tag.indexOf("=") >= 0) {
|
||||||
|
|
||||||
|
|
||||||
const split = Utils.SplitFirst(tag, "=");
|
const split = Utils.SplitFirst(tag, "=");
|
||||||
if(split[1] == "*"){
|
if (split[1] == "*") {
|
||||||
throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead`
|
throw `Error while parsing tag '${tag}' in ${context}: detected a wildcard on a normal value. Use a regex pattern instead`
|
||||||
}
|
}
|
||||||
return new Tag(split[0], split[1])
|
return new Tag(split[0], split[1])
|
||||||
}
|
}
|
||||||
|
throw `Error while parsing tag '${tag}' in ${context}: no key part and value part were found`
|
||||||
|
|
||||||
|
}
|
||||||
if (json.and !== undefined) {
|
if (json.and !== undefined) {
|
||||||
return new And(json.and.map(t => FromJSON.Tag(t, context)));
|
return new And(json.and.map(t => FromJSON.Tag(t, context)));
|
||||||
}
|
}
|
||||||
|
|
|
@ -253,20 +253,13 @@ export default class LayerConfig {
|
||||||
let sourceParts = iconUrl.split(";");
|
let sourceParts = iconUrl.split(";");
|
||||||
|
|
||||||
function genHtmlFromString(sourcePart: string): UIElement {
|
function genHtmlFromString(sourcePart: string): UIElement {
|
||||||
const style = `width:100%;height:100%;rotate:${rotation};display:block;position: absolute; top: 0, left: 0`;
|
const style = `width:100%;height:100%;transform: rotate( ${rotation} );display:block;position: absolute; top: 0, left: 0`;
|
||||||
let html: UIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
let html: UIElement = new FixedUiElement(`<img src="${sourcePart}" style="${style}" />`);
|
||||||
const match = sourcePart.match(/([a-zA-Z0-9_]*):#([0-9a-fA-F]{3,6})/)
|
const match = sourcePart.match(/([a-zA-Z0-9_]*):([^;]*)/)
|
||||||
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
if (match !== null && Svg.All[match[1] + ".svg"] !== undefined) {
|
||||||
html = new Combine([
|
html = new Combine([
|
||||||
(Svg.All[match[1] + ".svg"] as string)
|
(Svg.All[match[1] + ".svg"] as string)
|
||||||
.replace(/#000000/g, "#" + match[2])
|
.replace(/#000000/g, match[2])
|
||||||
]).SetStyle(style);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourcePart.startsWith(Utils.assets_path)) {
|
|
||||||
const key = sourcePart.substr(Utils.assets_path.length);
|
|
||||||
html = new Combine([
|
|
||||||
(Svg.All[key] as string).replace(/stop-color:#000000/g, 'stop-color:' + color)
|
|
||||||
]).SetStyle(style);
|
]).SetStyle(style);
|
||||||
}
|
}
|
||||||
return html;
|
return html;
|
||||||
|
|
|
@ -1,53 +1,45 @@
|
||||||
import Translations from "./UI/i18n/Translations";
|
|
||||||
import {TabbedComponent} from "./UI/Base/TabbedComponent";
|
|
||||||
import {ShareScreen} from "./UI/ShareScreen";
|
|
||||||
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
import {FixedUiElement} from "./UI/Base/FixedUiElement";
|
||||||
import CheckBox from "./UI/Input/CheckBox";
|
import CheckBox from "./UI/Input/CheckBox";
|
||||||
import Combine from "./UI/Base/Combine";
|
import Combine from "./UI/Base/Combine";
|
||||||
import {UIElement} from "./UI/UIElement";
|
import {Basemap} from "./UI/BigComponents/Basemap";
|
||||||
import {MoreScreen} from "./UI/MoreScreen";
|
|
||||||
import {FilteredLayer} from "./Logic/FilteredLayer";
|
|
||||||
import {Basemap} from "./UI/Basemap";
|
|
||||||
import State from "./State";
|
import State from "./State";
|
||||||
import {WelcomeMessage} from "./UI/WelcomeMessage";
|
|
||||||
import {LayerSelection} from "./UI/LayerSelection";
|
|
||||||
import {VariableUiElement} from "./UI/Base/VariableUIElement";
|
|
||||||
import LoadFromOverpass from "./Logic/Actors/UpdateFromOverpass";
|
import LoadFromOverpass from "./Logic/Actors/UpdateFromOverpass";
|
||||||
import {UIEventSource} from "./Logic/UIEventSource";
|
import {UIEventSource} from "./Logic/UIEventSource";
|
||||||
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
import {QueryParameters} from "./Logic/Web/QueryParameters";
|
||||||
import {PersonalLayersPanel} from "./UI/PersonalLayersPanel";
|
import StrayClickHandler from "./Logic/Actors/StrayClickHandler";
|
||||||
import Locale from "./UI/i18n/Locale";
|
import SimpleAddUI from "./UI/BigComponents/SimpleAddUI";
|
||||||
import {StrayClickHandler} from "./Logic/Actors/StrayClickHandler";
|
import CenterMessageBox from "./UI/CenterMessageBox";
|
||||||
import {SimpleAddUI} from "./UI/SimpleAddUI";
|
|
||||||
import {CenterMessageBox} from "./UI/CenterMessageBox";
|
|
||||||
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
|
||||||
import {TagUtils} from "./Logic/Tags";
|
import {TagUtils} from "./Logic/Tags";
|
||||||
import {UserBadge} from "./UI/UserBadge";
|
import UserBadge from "./UI/BigComponents/UserBadge";
|
||||||
import {SearchAndGo} from "./UI/SearchAndGo";
|
import SearchAndGo from "./UI/BigComponents/SearchAndGo";
|
||||||
import {FullScreenMessageBox} from "./UI/FullScreenMessageBoxHandler";
|
import FullScreenMessageBox from "./UI/FullScreenMessageBoxHandler";
|
||||||
import {GeoLocationHandler} from "./Logic/Actors/GeoLocationHandler";
|
import GeoLocationHandler from "./Logic/Actors/GeoLocationHandler";
|
||||||
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
|
||||||
import {Utils} from "./Utils";
|
import {Utils} from "./Utils";
|
||||||
import BackgroundSelector from "./UI/BackgroundSelector";
|
import FeatureInfoBox from "./UI/Popup/FeatureInfoBox";
|
||||||
import {FeatureInfoBox} from "./UI/Popup/FeatureInfoBox";
|
|
||||||
import Svg from "./Svg";
|
import Svg from "./Svg";
|
||||||
import Link from "./UI/Base/Link";
|
import Link from "./UI/Base/Link";
|
||||||
import * as personal from "./assets/themes/personalLayout/personalLayout.json"
|
import * as personal from "./assets/themes/personalLayout/personalLayout.json"
|
||||||
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||||
import * as L from "leaflet";
|
import * as L from "leaflet";
|
||||||
import Img from "./UI/Base/Img";
|
import Img from "./UI/Base/Img";
|
||||||
import {UserDetails} from "./Logic/Osm/OsmConnection";
|
import UserDetails from "./Logic/Osm/OsmConnection";
|
||||||
import Attribution from "./UI/Misc/Attribution";
|
import Attribution from "./UI/BigComponents/Attribution";
|
||||||
import Constants from "./Models/Constants";
|
|
||||||
import MetaTagging from "./Logic/MetaTagging";
|
import MetaTagging from "./Logic/MetaTagging";
|
||||||
import FeatureSourceMerger from "./Logic/FeatureSource/FeatureSourceMerger";
|
import FeatureSourceMerger from "./Logic/FeatureSource/FeatureSourceMerger";
|
||||||
import RememberingSource from "./Logic/FeatureSource/RememberingSource";
|
import RememberingSource from "./Logic/FeatureSource/RememberingSource";
|
||||||
import FilteringFeatureSource from "./Logic/FeatureSource/FilteringFeatureSource";
|
import FilteringFeatureSource from "./Logic/FeatureSource/FilteringFeatureSource";
|
||||||
import WayHandlingApplyingFeatureSource from "./Logic/FeatureSource/WayHandlingApplyingFeatureSource";
|
import WayHandlingApplyingFeatureSource from "./Logic/FeatureSource/WayHandlingApplyingFeatureSource";
|
||||||
import FeatureSource from "./Logic/FeatureSource/FeatureSource";
|
|
||||||
import NoOverlapSource from "./Logic/FeatureSource/NoOverlapSource";
|
import NoOverlapSource from "./Logic/FeatureSource/NoOverlapSource";
|
||||||
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
import AvailableBaseLayers from "./Logic/Actors/AvailableBaseLayers";
|
||||||
import LayerResetter from "./Logic/Actors/LayerResetter";
|
import LayerResetter from "./Logic/Actors/LayerResetter";
|
||||||
|
import FullWelcomePaneWithTabs from "./UI/BigComponents/FullWelcomePaneWithTabs";
|
||||||
|
import LayerControlPanel from "./UI/BigComponents/LayerControlPanel";
|
||||||
|
import FeatureSwitched from "./UI/Base/FeatureSwitched";
|
||||||
|
import FeatureDuplicatorPerLayer from "./Logic/FeatureSource/FeatureDuplicatorPerLayer";
|
||||||
|
import LayerConfig from "./Customizations/JSON/LayerConfig";
|
||||||
|
import ShowDataLayer from "./UI/ShowDataLayer";
|
||||||
|
|
||||||
export class InitUiElements {
|
export class InitUiElements {
|
||||||
|
|
||||||
|
@ -87,10 +79,6 @@ export class InitUiElements {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
InitUiElements.InitBaseMap();
|
InitUiElements.InitBaseMap();
|
||||||
|
|
||||||
new FixedUiElement("").AttachTo("decoration-desktop"); // Remove the decoration
|
new FixedUiElement("").AttachTo("decoration-desktop"); // Remove the decoration
|
||||||
|
@ -216,13 +204,15 @@ export class InitUiElements {
|
||||||
marker.addTo(State.state.leafletMap.data)
|
marker.addTo(State.state.leafletMap.data)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new FeatureSwitched(
|
||||||
new GeoLocationHandler(
|
new GeoLocationHandler(
|
||||||
State.state.currentGPSLocation,
|
State.state.currentGPSLocation,
|
||||||
State.state.leafletMap,
|
State.state.leafletMap
|
||||||
State.state.featureSwitchGeolocation
|
|
||||||
)
|
)
|
||||||
.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;`)
|
.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;`)
|
||||||
|
, State.state.featureSwitchGeolocation)
|
||||||
.AttachTo("geolocate-button");
|
.AttachTo("geolocate-button");
|
||||||
|
|
||||||
State.state.locationControl.ping();
|
State.state.locationControl.ping();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -254,23 +244,18 @@ export class InitUiElements {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static OnlyIf(featureSwitch: UIEventSource<boolean>, callback: () => void) {
|
private static OnlyIf(featureSwitch: UIEventSource<boolean>, callback: () => void) {
|
||||||
featureSwitch.addCallback(() => {
|
featureSwitch.addCallbackAndRun(() => {
|
||||||
|
|
||||||
if (featureSwitch.data) {
|
if (featureSwitch.data) {
|
||||||
callback();
|
callback();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (featureSwitch.data) {
|
|
||||||
callback();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private static InitWelcomeMessage() {
|
||||||
|
|
||||||
static InitWelcomeMessage() {
|
const fullOptions = new FullWelcomePaneWithTabs();
|
||||||
|
|
||||||
const fullOptions = this.CreateWelcomePane();
|
|
||||||
|
|
||||||
const help = Svg.help_svg().SetClass("open-welcome-button");
|
const help = Svg.help_svg().SetClass("open-welcome-button");
|
||||||
const close = Svg.close_svg().SetClass("close-welcome-button");
|
const close = Svg.close_svg().SetClass("close-welcome-button");
|
||||||
|
@ -298,7 +283,7 @@ export class InitUiElements {
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
const fullOptions2 = this.CreateWelcomePane();
|
const fullOptions2 = new FullWelcomePaneWithTabs();
|
||||||
State.state.fullScreenMessage.setData(fullOptions2)
|
State.state.fullScreenMessage.setData(fullOptions2)
|
||||||
|
|
||||||
Svg.help_svg()
|
Svg.help_svg()
|
||||||
|
@ -311,15 +296,11 @@ export class InitUiElements {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static InitLayerSelection() {
|
private static InitLayerSelection() {
|
||||||
InitUiElements.OnlyIf(State.state.featureSwitchLayers, () => {
|
InitUiElements.OnlyIf(State.state.featureSwitchLayers, () => {
|
||||||
|
|
||||||
const layerControlPanel = this.GenerateLayerControlPanel();
|
const layerControlPanel = new LayerControlPanel()
|
||||||
if (layerControlPanel === undefined) {
|
.SetStyle("display:block;padding:0.75em;border-radius:1em;");
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 closeButton = Svg.close_svg().SetClass("layer-selection-toggle").SetStyle(" background: var(--subtle-detail-color);")
|
||||||
const checkbox = new CheckBox(
|
const checkbox = new CheckBox(
|
||||||
new Combine([
|
new Combine([
|
||||||
|
@ -334,18 +315,19 @@ export class InitUiElements {
|
||||||
checkbox.AttachTo("layer-selection");
|
checkbox.AttachTo("layer-selection");
|
||||||
|
|
||||||
|
|
||||||
State.state.locationControl.addCallback(() => {
|
State.state.locationControl
|
||||||
|
.addCallback(() => {
|
||||||
// Close the layer selection when the map is moved
|
// Close the layer selection when the map is moved
|
||||||
checkbox.isEnabled.setData(false);
|
// checkbox.isEnabled.setData(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const fullScreen = this.GenerateLayerControlPanel();
|
const fullScreen = new LayerControlPanel();
|
||||||
checkbox.isEnabled.addCallback(isEnabled => {
|
checkbox.isEnabled.addCallback(isEnabled => {
|
||||||
if (isEnabled) {
|
if (isEnabled) {
|
||||||
State.state.fullScreenMessage.setData(fullScreen);
|
State.state.fullScreenMessage.setData(fullScreen);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
State.state.fullScreenMessage.addCallbackAndRun(latest => {
|
State.state.fullScreenMessage.addCallback(latest => {
|
||||||
if (latest === undefined) {
|
if (latest === undefined) {
|
||||||
checkbox.isEnabled.setData(false);
|
checkbox.isEnabled.setData(false);
|
||||||
}
|
}
|
||||||
|
@ -354,7 +336,7 @@ export class InitUiElements {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static InitBaseMap() {
|
private static InitBaseMap() {
|
||||||
|
|
||||||
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
|
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state.locationControl).availableEditorLayers;
|
||||||
State.state.backgroundLayer = QueryParameters.GetQueryParameter("background",
|
State.state.backgroundLayer = QueryParameters.GetQueryParameter("background",
|
||||||
|
@ -371,8 +353,6 @@ export class InitUiElements {
|
||||||
}, [], layer => layer.id);
|
}, [], layer => layer.id);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
new LayerResetter(
|
new LayerResetter(
|
||||||
State.state.backgroundLayer, State.state.locationControl,
|
State.state.backgroundLayer, State.state.locationControl,
|
||||||
State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId));
|
State.state.availableBackgroundLayers, State.state.layoutToUse.map((layout: LayoutConfig) => layout.defaultBackgroundId));
|
||||||
|
@ -393,61 +373,33 @@ export class InitUiElements {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static InitLayers() {
|
private static InitLayers() {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const state = State.state;
|
const state = State.state;
|
||||||
const flayers: FilteredLayer[] = []
|
const flayers: { layerDef: LayerConfig, isDisplayed: UIEventSource<boolean> }[] = []
|
||||||
for (const layer of state.layoutToUse.data.layers) {
|
for (const layer of state.layoutToUse.data.layers) {
|
||||||
|
|
||||||
if (typeof (layer) === "string") {
|
if (typeof (layer) === "string") {
|
||||||
throw "Layer " + layer + " was not substituted";
|
throw "Layer " + layer + " was not substituted";
|
||||||
}
|
}
|
||||||
|
|
||||||
let generateContents = (tags: UIEventSource<any>) => new FeatureInfoBox(tags, layer);
|
const isDisplayed = QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown")
|
||||||
if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
|
.map<boolean>((str) => str !== "false", [], (b) => b.toString());
|
||||||
generateContents = undefined;
|
const flayer = {
|
||||||
|
isDisplayed: isDisplayed,
|
||||||
|
layerDef: layer
|
||||||
}
|
}
|
||||||
|
|
||||||
const flayer: FilteredLayer = new FilteredLayer(layer, generateContents);
|
|
||||||
flayers.push(flayer);
|
flayers.push(flayer);
|
||||||
|
|
||||||
QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wether or not layer " + layer.id + " is shown")
|
|
||||||
.map<boolean>((str) => str !== "false", [], (b) => b.toString())
|
|
||||||
.syncWith(
|
|
||||||
flayer.isDisplayed
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
State.state.filteredLayers.setData(flayers);
|
State.state.filteredLayers.setData(flayers);
|
||||||
|
|
||||||
function addMatchingIds(src: FeatureSource) {
|
|
||||||
|
|
||||||
src.features.addCallback(features => {
|
|
||||||
features.forEach(f => {
|
|
||||||
const properties = f.feature.properties;
|
|
||||||
if (properties._matching_layer_id) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const flayer of flayers) {
|
|
||||||
if (flayer.layerDef.overpassTags.matchesProperties(properties)) {
|
|
||||||
properties._matching_layer_id = flayer.layerDef.id;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
|
const updater = new LoadFromOverpass(state.locationControl, state.layoutToUse, state.leafletMap);
|
||||||
State.state.layerUpdater = updater;
|
State.state.layerUpdater = updater;
|
||||||
|
|
||||||
addMatchingIds(updater);
|
|
||||||
addMatchingIds(State.state.changes);
|
|
||||||
|
|
||||||
|
|
||||||
const source =
|
const source =
|
||||||
new FilteringFeatureSource(
|
new FilteringFeatureSource(
|
||||||
|
@ -455,9 +407,9 @@ export class InitUiElements {
|
||||||
State.state.locationControl,
|
State.state.locationControl,
|
||||||
new FeatureSourceMerger([
|
new FeatureSourceMerger([
|
||||||
new RememberingSource(new WayHandlingApplyingFeatureSource(flayers,
|
new RememberingSource(new WayHandlingApplyingFeatureSource(flayers,
|
||||||
new NoOverlapSource(flayers, updater)
|
new NoOverlapSource(flayers, new FeatureDuplicatorPerLayer(flayers, updater))
|
||||||
)),
|
)),
|
||||||
State.state.changes]));
|
new FeatureDuplicatorPerLayer(flayers, State.state.changes)]));
|
||||||
|
|
||||||
|
|
||||||
source.features.addCallback((featuresFreshness: { feature: any, freshness: Date }[]) => {
|
source.features.addCallback((featuresFreshness: { feature: any, freshness: Date }[]) => {
|
||||||
|
@ -466,26 +418,10 @@ export class InitUiElements {
|
||||||
State.state.allElements.addElement(feature);
|
State.state.allElements.addElement(feature);
|
||||||
})
|
})
|
||||||
MetaTagging.addMetatags(features);
|
MetaTagging.addMetatags(features);
|
||||||
|
|
||||||
function renderLayers(layers) {
|
|
||||||
|
|
||||||
|
|
||||||
if (layers.length === 0) {
|
|
||||||
if (features.length > 0) {
|
|
||||||
console.warn("Got some leftovers: ", features.join("; "))
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const layer = layers[0];
|
|
||||||
const rest = layers.slice(1, layers.length);
|
|
||||||
features = layer.SetApplicableData(features);
|
|
||||||
renderLayers(rest);
|
|
||||||
}
|
|
||||||
|
|
||||||
renderLayers(flayers);
|
|
||||||
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
new ShowDataLayer(source.features, State.state.leafletMap, flayers);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -528,72 +464,4 @@ export class InitUiElements {
|
||||||
new CenterMessageBox().AttachTo("centermessage");
|
new CenterMessageBox().AttachTo("centermessage");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CreateWelcomePane() {
|
|
||||||
|
|
||||||
const layoutToUse = State.state.layoutToUse.data;
|
|
||||||
let welcome: UIElement = new WelcomeMessage();
|
|
||||||
if (layoutToUse.id === personal.id) {
|
|
||||||
welcome = new PersonalLayersPanel();
|
|
||||||
}
|
|
||||||
|
|
||||||
const tabs = [
|
|
||||||
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
|
||||||
{
|
|
||||||
header: Svg.osm_logo_img,
|
|
||||||
content: Translations.t.general.openStreetMapIntro as UIElement
|
|
||||||
},
|
|
||||||
|
|
||||||
]
|
|
||||||
|
|
||||||
if (State.state.featureSwitchShareScreen.data) {
|
|
||||||
tabs.push({header: Svg.share_img, content: new ShareScreen()});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State.state.featureSwitchMoreQuests.data) {
|
|
||||||
|
|
||||||
tabs.push({
|
|
||||||
header: Svg.add_img,
|
|
||||||
content: new MoreScreen()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
tabs.push({
|
|
||||||
header: Svg.help,
|
|
||||||
content: new VariableUiElement(State.state.osmConnection.userDetails.map(userdetails => {
|
|
||||||
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).Render();
|
|
||||||
}, [Locale.language]))
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
return new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab)
|
|
||||||
.ListenTo(State.state.osmConnection.userDetails);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private static GenerateLayerControlPanel() {
|
|
||||||
|
|
||||||
|
|
||||||
let layerControlPanel: UIElement = undefined;
|
|
||||||
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
|
||||||
layerControlPanel = new BackgroundSelector();
|
|
||||||
layerControlPanel.SetStyle("margin:1em");
|
|
||||||
layerControlPanel.onClick(() => {
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (State.state.filteredLayers.data.length > 1) {
|
|
||||||
const layerSelection = new LayerSelection();
|
|
||||||
layerSelection.onClick(() => {
|
|
||||||
});
|
|
||||||
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
|
||||||
}
|
|
||||||
return layerControlPanel;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -5,7 +5,7 @@ import {Utils} from "../../Utils";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
|
|
||||||
export class GeoLocationHandler extends UIElement {
|
export default class GeoLocationHandler extends UIElement {
|
||||||
|
|
||||||
private readonly _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
private readonly _isActive: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
private readonly _permission: UIEventSource<string> = new UIEventSource<string>("");
|
private readonly _permission: UIEventSource<string> = new UIEventSource<string>("");
|
||||||
|
@ -13,17 +13,14 @@ export class GeoLocationHandler extends UIElement {
|
||||||
private readonly _hasLocation: UIEventSource<boolean>;
|
private readonly _hasLocation: UIEventSource<boolean>;
|
||||||
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
|
private readonly _currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>;
|
||||||
private readonly _leafletMap: UIEventSource<L.Map>;
|
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||||
private readonly _featureSwitch: UIEventSource<boolean>;
|
|
||||||
|
|
||||||
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
constructor(currentGPSLocation: UIEventSource<{ latlng: any; accuracy: number }>,
|
||||||
leafletMap: UIEventSource<L.Map>,
|
leafletMap: UIEventSource<L.Map>) {
|
||||||
featureSwitch: UIEventSource<boolean>) {
|
|
||||||
super(undefined);
|
super(undefined);
|
||||||
this._currentGPSLocation = currentGPSLocation;
|
this._currentGPSLocation = currentGPSLocation;
|
||||||
this._leafletMap = leafletMap;
|
this._leafletMap = leafletMap;
|
||||||
this._featureSwitch = featureSwitch;
|
|
||||||
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
this._hasLocation = currentGPSLocation.map((location) => location !== undefined);
|
||||||
var self = this;
|
const self = this;
|
||||||
import("../../vendor/Leaflet.AccuratePosition.js").then(() => {
|
import("../../vendor/Leaflet.AccuratePosition.js").then(() => {
|
||||||
self.init();
|
self.init();
|
||||||
})
|
})
|
||||||
|
@ -92,10 +89,6 @@ export class GeoLocationHandler extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
InnerRender(): string {
|
||||||
if (!this._featureSwitch.data) {
|
|
||||||
return "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this._hasLocation.data) {
|
if (this._hasLocation.data) {
|
||||||
return Svg.crosshair_blue_img;
|
return Svg.crosshair_blue_img;
|
||||||
}
|
}
|
||||||
|
@ -124,7 +117,7 @@ export class GeoLocationHandler extends UIElement {
|
||||||
|
|
||||||
private StartGeolocating(zoomlevel = 19) {
|
private StartGeolocating(zoomlevel = 19) {
|
||||||
const self = this;
|
const self = this;
|
||||||
const map : any = this._leafletMap.data;
|
const map: any = this._leafletMap.data;
|
||||||
if (self._permission.data === "denied") {
|
if (self._permission.data === "denied") {
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,14 +8,14 @@ import Img from "../../UI/Base/Img";
|
||||||
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
* The stray-click-hanlders adds a marker to the map if no feature was clicked.
|
||||||
* Shows the given uiToShow-element in the messagebox
|
* Shows the given uiToShow-element in the messagebox
|
||||||
*/
|
*/
|
||||||
export class StrayClickHandler {
|
export default class StrayClickHandler {
|
||||||
private _lastMarker;
|
private _lastMarker;
|
||||||
private _uiToShow: (() => UIElement);
|
private _uiToShow: (() => UIElement);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
lastClickLocation: UIEventSource<{ lat: number, lon:number }>,
|
lastClickLocation: UIEventSource<{ lat: number, lon: number }>,
|
||||||
selectedElement: UIEventSource<string>,
|
selectedElement: UIEventSource<string>,
|
||||||
filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean>}[]>,
|
filteredLayers: UIEventSource<{ readonly isDisplayed: UIEventSource<boolean> }[]>,
|
||||||
leafletMap: UIEventSource<L.Map>,
|
leafletMap: UIEventSource<L.Map>,
|
||||||
fullscreenMessage: UIEventSource<UIElement>,
|
fullscreenMessage: UIEventSource<UIElement>,
|
||||||
uiToShow: (() => UIElement)) {
|
uiToShow: (() => UIElement)) {
|
||||||
|
@ -23,7 +23,7 @@ export class StrayClickHandler {
|
||||||
const self = this;
|
const self = this;
|
||||||
filteredLayers.data.forEach((filteredLayer) => {
|
filteredLayers.data.forEach((filteredLayer) => {
|
||||||
filteredLayer.isDisplayed.addCallback(isEnabled => {
|
filteredLayer.isDisplayed.addCallback(isEnabled => {
|
||||||
if(isEnabled && self._lastMarker && leafletMap.data !== undefined){
|
if (isEnabled && self._lastMarker && leafletMap.data !== undefined) {
|
||||||
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
// When a layer is activated, we remove the 'last click location' in order to force the user to reclick
|
||||||
// This reclick might be at a location where a feature now appeared...
|
// This reclick might be at a location where a feature now appeared...
|
||||||
leafletMap.data.removeLayer(self._lastMarker);
|
leafletMap.data.removeLayer(self._lastMarker);
|
||||||
|
|
|
@ -38,6 +38,7 @@ export default class UpdateFromOverpass implements FeatureSource{
|
||||||
location: UIEventSource<Loc>,
|
location: UIEventSource<Loc>,
|
||||||
layoutToUse: UIEventSource<LayoutConfig>,
|
layoutToUse: UIEventSource<LayoutConfig>,
|
||||||
leafletMap: UIEventSource<L.Map>) {
|
leafletMap: UIEventSource<L.Map>) {
|
||||||
|
console.log("Crating overpass updater")
|
||||||
this._location = location;
|
this._location = location;
|
||||||
this._layoutToUse = layoutToUse;
|
this._layoutToUse = layoutToUse;
|
||||||
this._leafletMap = leafletMap;
|
this._leafletMap = leafletMap;
|
||||||
|
|
68
Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
Normal file
68
Logic/FeatureSource/FeatureDuplicatorPerLayer.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
import FeatureSource from "./FeatureSource";
|
||||||
|
import {UIEventSource} from "../UIEventSource";
|
||||||
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* In some rare cases, some elements are shown on multiple layers (when 'passthrough' is enabled)
|
||||||
|
* If this is the case, multiple objects with a different _matching_layer_id are generated.
|
||||||
|
* If not, the _feature_layter_id is added
|
||||||
|
*/
|
||||||
|
export default class FeatureDuplicatorPerLayer implements FeatureSource {
|
||||||
|
public readonly features: UIEventSource<{ feature: any; freshness: Date }[]>;
|
||||||
|
|
||||||
|
|
||||||
|
constructor(layers: { layerDef: LayerConfig }[], upstream: FeatureSource) {
|
||||||
|
let noPassthroughts = true;
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.layerDef.passAllFeatures) {
|
||||||
|
noPassthroughts = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.features = upstream.features.map(features => {
|
||||||
|
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||||
|
if(features === undefined){
|
||||||
|
return newFeatures;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
for (const f of features) {
|
||||||
|
if (f.feature._matching_layer_id) {
|
||||||
|
// Already matched previously
|
||||||
|
// We simply add it
|
||||||
|
newFeatures.push(f);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const layer of layers) {
|
||||||
|
if (layer.layerDef.overpassTags.matchesProperties(f.feature.properties)) {
|
||||||
|
if (layer.layerDef.passAllFeatures) {
|
||||||
|
|
||||||
|
// We copy the feature; the "properties" field is kept identical though!
|
||||||
|
// Keeping "properties" identical is needed, as it might break the 'allElementStorage' otherwise
|
||||||
|
const newFeature = {
|
||||||
|
geometry: f.feature.geometry,
|
||||||
|
id: f.feature.id,
|
||||||
|
type: f.feature.type,
|
||||||
|
properties: f.feature.properties,
|
||||||
|
_matching_layer_id : layer.layerDef.id
|
||||||
|
}
|
||||||
|
newFeatures.push({feature: newFeature, freshness: f.freshness});
|
||||||
|
} else {
|
||||||
|
// If not 'passAllFeatures', we are done
|
||||||
|
f.feature._matching_layer_id = layer.layerDef.id;
|
||||||
|
newFeatures.push(f);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newFeatures;
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -18,7 +18,7 @@ export default class FeatureSourceMerger implements FeatureSource {
|
||||||
let all = {}; // Mapping 'id' -> {feature, freshness}
|
let all = {}; // Mapping 'id' -> {feature, freshness}
|
||||||
for (const source of this._sources) {
|
for (const source of this._sources) {
|
||||||
for (const f of source.features.data) {
|
for (const f of source.features.data) {
|
||||||
const id = f.feature.properties.id+f.feature.geometry.type;
|
const id = f.feature.properties.id+f.feature.geometry.type+f.feature._matching_layer_id;
|
||||||
const oldV = all[id];
|
const oldV = all[id];
|
||||||
if(oldV === undefined){
|
if(oldV === undefined){
|
||||||
all[id] = f;
|
all[id] = f;
|
||||||
|
|
|
@ -18,10 +18,9 @@ export default class FilteringFeatureSource implements FeatureSource {
|
||||||
const self = this;
|
const self = this;
|
||||||
|
|
||||||
function update() {
|
function update() {
|
||||||
console.log("UPdating...")
|
|
||||||
const features: { feature: any, freshness: Date }[] = upstream.features.data;
|
const features: { feature: any, freshness: Date }[] = upstream.features.data;
|
||||||
const newFeatures = features.filter(f => {
|
const newFeatures = features.filter(f => {
|
||||||
const layerId = f.feature.properties._matching_layer_id;
|
const layerId = f.feature._matching_layer_id;
|
||||||
if (layerId === undefined) {
|
if (layerId === undefined) {
|
||||||
console.error(f)
|
console.error(f)
|
||||||
throw "feature._matching_layer_id is undefined"
|
throw "feature._matching_layer_id is undefined"
|
||||||
|
@ -37,16 +36,22 @@ export default class FilteringFeatureSource implements FeatureSource {
|
||||||
});
|
});
|
||||||
self.features.setData(newFeatures);
|
self.features.setData(newFeatures);
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const layer of layers) {
|
for (const layer of layers) {
|
||||||
layerDict[layer.layerDef.id] = layer;
|
layerDict[layer.layerDef.id] = layer;
|
||||||
layer.isDisplayed.addCallback(update)
|
layer.isDisplayed.addCallback(() => {
|
||||||
|
console.log("Updating due to layer change")
|
||||||
|
update()})
|
||||||
}
|
}
|
||||||
upstream.features.addCallback(update);
|
upstream.features.addCallback(() => {
|
||||||
location.map(l => l.zoom).addCallback(update);
|
console.log("Updating due to upstream change")
|
||||||
|
update()});
|
||||||
|
location.map(l => l.zoom).addCallback(() => {
|
||||||
|
console.log("UPdating due to zoom level change")
|
||||||
|
update();});
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -45,7 +45,7 @@ export default class NoOverlapSource {
|
||||||
partitions[layerId] = []
|
partitions[layerId] = []
|
||||||
}
|
}
|
||||||
for (const feature of features) {
|
for (const feature of features) {
|
||||||
partitions[feature.feature.properties._matching_layer_id].push(feature);
|
partitions[feature.feature._matching_layer_id].push(feature);
|
||||||
}
|
}
|
||||||
|
|
||||||
// With this partitioning in hand, we run over every layer and remove every underlying feature if needed
|
// With this partitioning in hand, we run over every layer and remove every underlying feature if needed
|
||||||
|
|
|
@ -32,7 +32,7 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource {
|
||||||
const newFeatures: { feature: any, freshness: Date }[] = [];
|
const newFeatures: { feature: any, freshness: Date }[] = [];
|
||||||
for (const f of features) {
|
for (const f of features) {
|
||||||
const feat = f.feature;
|
const feat = f.feature;
|
||||||
const layerId = feat.properties._matching_layer_id;
|
const layerId = feat._matching_layer_id;
|
||||||
const layer: LayerConfig = layerDict[layerId].layerDef;
|
const layer: LayerConfig = layerDict[layerId].layerDef;
|
||||||
if (layer === undefined) {
|
if (layer === undefined) {
|
||||||
throw "No layer found with id " + layerId;
|
throw "No layer found with id " + layerId;
|
||||||
|
@ -50,6 +50,7 @@ export default class WayHandlingApplyingFeatureSource implements FeatureSource {
|
||||||
}
|
}
|
||||||
|
|
||||||
const centerPoint = GeoOperations.centerpoint(feat);
|
const centerPoint = GeoOperations.centerpoint(feat);
|
||||||
|
centerPoint._matching_layer_id = feat._matching_layer_id;
|
||||||
newFeatures.push({feature: centerPoint, freshness: f.freshness});
|
newFeatures.push({feature: centerPoint, freshness: f.freshness});
|
||||||
|
|
||||||
if(layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY){
|
if(layer.wayHandling === LayerConfig.WAYHANDLING_CENTER_AND_WAY){
|
||||||
|
|
|
@ -1,163 +0,0 @@
|
||||||
import {TagsFilter, TagUtils} from "./Tags";
|
|
||||||
import {UIEventSource} from "./UIEventSource";
|
|
||||||
import * as L from "leaflet"
|
|
||||||
import {Layer} from "leaflet"
|
|
||||||
import {GeoOperations} from "./GeoOperations";
|
|
||||||
import {UIElement} from "../UI/UIElement";
|
|
||||||
import State from "../State";
|
|
||||||
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
|
||||||
import Hash from "./Web/Hash";
|
|
||||||
import LazyElement from "../UI/Base/LazyElement";
|
|
||||||
|
|
||||||
/***
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
export class FilteredLayer {
|
|
||||||
|
|
||||||
public readonly name: string | UIElement;
|
|
||||||
public readonly isDisplayed: UIEventSource<boolean> = new UIEventSource(true);
|
|
||||||
public readonly layerDef: LayerConfig;
|
|
||||||
|
|
||||||
private readonly filters: TagsFilter;
|
|
||||||
private readonly _maxAllowedOverlap: number;
|
|
||||||
|
|
||||||
/** The featurecollection from overpass
|
|
||||||
*/
|
|
||||||
private _dataFromOverpass: any[];
|
|
||||||
/**
|
|
||||||
* The leaflet layer object which should be removed on rerendering
|
|
||||||
*/
|
|
||||||
private _geolayer;
|
|
||||||
|
|
||||||
private _showOnPopup: (tags: UIEventSource<any>, feature: any) => UIElement;
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
layerDef: LayerConfig,
|
|
||||||
showOnPopup: ((tags: UIEventSource<any>, feature: any) => UIElement)
|
|
||||||
) {
|
|
||||||
this.layerDef = layerDef;
|
|
||||||
|
|
||||||
this._showOnPopup = showOnPopup;
|
|
||||||
this.name = name;
|
|
||||||
this.filters = layerDef.overpassTags;
|
|
||||||
this._maxAllowedOverlap = layerDef.hideUnderlayingFeaturesMinPercentage;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
public SetApplicableData(features: any[]): any[] {
|
|
||||||
const leftoverFeatures = [];
|
|
||||||
const selfFeatures = [];
|
|
||||||
for (let feature of features) {
|
|
||||||
const tags = TagUtils.proprtiesToKV(feature.properties);
|
|
||||||
const matches = this.filters.matches(tags);
|
|
||||||
if (matches) {
|
|
||||||
selfFeatures.push(feature);
|
|
||||||
}
|
|
||||||
if (!matches || this.layerDef.passAllFeatures) {
|
|
||||||
leftoverFeatures.push(feature);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.RenderLayer(selfFeatures)
|
|
||||||
return leftoverFeatures;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private RenderLayer(features: any[]) {
|
|
||||||
|
|
||||||
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.leafletMap.data.removeLayer(this._geolayer);
|
|
||||||
}
|
|
||||||
|
|
||||||
// We fetch all the data we have to show:
|
|
||||||
const data = {
|
|
||||||
type: "FeatureCollection",
|
|
||||||
features: features
|
|
||||||
}
|
|
||||||
|
|
||||||
let self = this;
|
|
||||||
this._geolayer = L.geoJSON(data, {
|
|
||||||
style: feature => {
|
|
||||||
const tagsSource = State.state.allElements.getEventSourceFor(feature);
|
|
||||||
return self.layerDef.GenerateLeafletStyle(tagsSource, 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 tagSource = State.state.allElements.getEventSourceFor(feature);
|
|
||||||
|
|
||||||
const style = self.layerDef.GenerateLeafletStyle(tagSource, self._showOnPopup !== undefined);
|
|
||||||
let marker;
|
|
||||||
if (style.icon === undefined) {
|
|
||||||
marker = L.circle(latLng, {
|
|
||||||
radius: 25,
|
|
||||||
color: style.color
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
marker = L.marker(latLng, {
|
|
||||||
icon: L.divIcon({
|
|
||||||
html: style.icon.html.Render(),
|
|
||||||
className: style.icon.className,
|
|
||||||
iconAnchor: style.icon.iconAnchor,
|
|
||||||
iconUrl: style.icon.iconUrl,
|
|
||||||
popupAnchor: style.icon.popupAnchor,
|
|
||||||
iconSize: style.icon.iconSize
|
|
||||||
})
|
|
||||||
});
|
|
||||||
}
|
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
const eventSource = State.state.allElements.getEventSourceFor(feature);
|
|
||||||
let uiElement: LazyElement = new LazyElement(() => 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...
|
|
||||||
uiElement.Activate();
|
|
||||||
State.state.selectedElement.setData(feature);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
|
|
||||||
// This element is in the URL, so this is a share link
|
|
||||||
// We already open it
|
|
||||||
uiElement.Activate();
|
|
||||||
popup.setContent(uiElement.Render());
|
|
||||||
|
|
||||||
const center = GeoOperations.centerpoint(feature).geometry.coordinates;
|
|
||||||
popup.setLatLng({lat: center[1], lng: center[0]});
|
|
||||||
popup.openOn(State.state.leafletMap.data);
|
|
||||||
State.state.selectedElement.setData(feature);
|
|
||||||
uiElement.Update();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this._geolayer.addTo(State.state.leafletMap.data);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
|
@ -8,7 +8,7 @@ import Svg from "../../Svg";
|
||||||
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Img from "../../UI/Base/Img";
|
import Img from "../../UI/Base/Img";
|
||||||
|
|
||||||
export class UserDetails {
|
export default class UserDetails {
|
||||||
|
|
||||||
public loggedIn = false;
|
public loggedIn = false;
|
||||||
public name = "Not logged in";
|
public name = "Not logged in";
|
||||||
|
@ -29,7 +29,7 @@ export class OsmConnection {
|
||||||
public preferencesHandler: OsmPreferences;
|
public preferencesHandler: OsmPreferences;
|
||||||
public changesetHandler: ChangesetHandler;
|
public changesetHandler: ChangesetHandler;
|
||||||
|
|
||||||
private _onLoggedIn : ((userDetails: UserDetails) => void)[] = [];
|
private _onLoggedIn: ((userDetails: UserDetails) => void)[] = [];
|
||||||
|
|
||||||
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
|
constructor(dryRun: boolean, oauth_token: UIEventSource<string>,
|
||||||
// Used to keep multiple changesets open and to write to the correct changeset
|
// Used to keep multiple changesets open and to write to the correct changeset
|
||||||
|
@ -48,7 +48,7 @@ export class OsmConnection {
|
||||||
const iframeMode = window !== window.top;
|
const iframeMode = window !== window.top;
|
||||||
|
|
||||||
|
|
||||||
if ( iframeMode || pwaStandAloneMode || !singlePage) {
|
if (iframeMode || pwaStandAloneMode || !singlePage) {
|
||||||
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
// In standalone mode, we DON'T use single page login, as 'redirecting' opens a new window anyway...
|
||||||
// Same for an iframe...
|
// Same for an iframe...
|
||||||
this.auth = new osmAuth({
|
this.auth = new osmAuth({
|
||||||
|
@ -100,7 +100,8 @@ export class OsmConnection {
|
||||||
layout: LayoutConfig,
|
layout: LayoutConfig,
|
||||||
allElements: ElementStorage,
|
allElements: ElementStorage,
|
||||||
generateChangeXML: (csid: string) => string,
|
generateChangeXML: (csid: string) => string,
|
||||||
continuation: () => void = () => {}) {
|
continuation: () => void = () => {
|
||||||
|
}) {
|
||||||
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
|
this.changesetHandler.UploadChangeset(layout, allElements, generateChangeXML, continuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,7 +113,7 @@ export class OsmConnection {
|
||||||
return this.preferencesHandler.GetLongPreference(key, prefix);
|
return this.preferencesHandler.GetLongPreference(key, prefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
public OnLoggedIn(action: (userDetails: UserDetails) => void){
|
public OnLoggedIn(action: (userDetails: UserDetails) => void) {
|
||||||
this._onLoggedIn.push(action);
|
this._onLoggedIn.push(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -132,7 +133,7 @@ export class OsmConnection {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
path: '/api/0.6/user/details'
|
path: '/api/0.6/user/details'
|
||||||
}, function (err, details) {
|
}, function (err, details) {
|
||||||
if(err != null){
|
if (err != null) {
|
||||||
console.log(err);
|
console.log(err);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import {TileLayer} from "leaflet";
|
import {TileLayer} from "leaflet";
|
||||||
|
|
||||||
export interface BaseLayer {
|
export default interface BaseLayer {
|
||||||
id: string,
|
id: string,
|
||||||
name: string,
|
name: string,
|
||||||
layer: TileLayer,
|
layer: TileLayer,
|
||||||
|
|
9
State.ts
9
State.ts
|
@ -12,7 +12,7 @@ import LayoutConfig from "./Customizations/JSON/LayoutConfig";
|
||||||
import Hash from "./Logic/Web/Hash";
|
import Hash from "./Logic/Web/Hash";
|
||||||
import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
|
import {MangroveIdentity} from "./Logic/Web/MangroveReviews";
|
||||||
import InstalledThemes from "./Logic/Actors/InstalledThemes";
|
import InstalledThemes from "./Logic/Actors/InstalledThemes";
|
||||||
import {BaseLayer} from "./Models/BaseLayer";
|
import BaseLayer from "./Models/BaseLayer";
|
||||||
import Loc from "./Models/Loc";
|
import Loc from "./Models/Loc";
|
||||||
import Constants from "./Models/Constants";
|
import Constants from "./Models/Constants";
|
||||||
|
|
||||||
|
@ -61,11 +61,9 @@ export default class State {
|
||||||
|
|
||||||
|
|
||||||
public filteredLayers: UIEventSource<{
|
public filteredLayers: UIEventSource<{
|
||||||
readonly name: string | UIElement;
|
|
||||||
readonly isDisplayed: UIEventSource<boolean>,
|
readonly isDisplayed: UIEventSource<boolean>,
|
||||||
readonly layerDef: LayerConfig;
|
readonly layerDef: LayerConfig;
|
||||||
}[]> = new UIEventSource<{
|
}[]> = new UIEventSource<{
|
||||||
readonly name: string | UIElement;
|
|
||||||
readonly isDisplayed: UIEventSource<boolean>,
|
readonly isDisplayed: UIEventSource<boolean>,
|
||||||
readonly layerDef: LayerConfig;
|
readonly layerDef: LayerConfig;
|
||||||
}[]>([])
|
}[]>([])
|
||||||
|
@ -114,7 +112,8 @@ export default class State {
|
||||||
public layoutDefinition: string;
|
public layoutDefinition: string;
|
||||||
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
|
public installedThemes: UIEventSource<{ layout: LayoutConfig; definition: string }[]>;
|
||||||
|
|
||||||
public layerControlIsOpened: UIEventSource<boolean> = QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Wether or not the layer control is shown")
|
public layerControlIsOpened: UIEventSource<boolean> =
|
||||||
|
QueryParameters.GetQueryParameter("layer-control-toggle", "false", "Whether or not the layer control is shown")
|
||||||
.map<boolean>((str) => str !== "false", [], b => "" + b)
|
.map<boolean>((str) => str !== "false", [], b => "" + b)
|
||||||
|
|
||||||
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 >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>(
|
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 >${Constants.userJourney.mapCompleteHelpUnlock} changesets)`).map<number>(
|
||||||
|
@ -153,8 +152,6 @@ export default class State {
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> {
|
function featSw(key: string, deflt: (layout: LayoutConfig) => boolean, documentation: string): UIEventSource<boolean> {
|
||||||
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
|
const queryParameterSource = QueryParameters.GetQueryParameter(key, undefined, documentation);
|
||||||
// I'm so sorry about someone trying to decipher this
|
// I'm so sorry about someone trying to decipher this
|
||||||
|
|
4
Svg.ts
4
Svg.ts
|
@ -89,12 +89,12 @@ export default class Svg {
|
||||||
public static delete_icon_svg() { return new FixedUiElement(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 delete_icon_ui() { return new FixedUiElement(Svg.delete_icon_img);}
|
||||||
|
|
||||||
public static direction = " <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\" id=\"svg8\" version=\"1.1\" viewBox=\"0 0 100 100\" height=\"100\" width=\"100\"> <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\" /> <path id=\"path821\" d=\"M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z\" style=\"fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" /> </svg> "
|
public static direction = " <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\" 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\" /> <path style=\"fill:#000000;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_img = Img.AsImageElement(Svg.direction)
|
public static direction_img = Img.AsImageElement(Svg.direction)
|
||||||
public static direction_svg() { return new FixedUiElement(Svg.direction);}
|
public static direction_svg() { return new FixedUiElement(Svg.direction);}
|
||||||
public static direction_ui() { return new FixedUiElement(Svg.direction_img);}
|
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\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\" id=\"svg8\" sodipodi:docname=\"direction_gradient.svg\" inkscape:version=\"0.92.4 (5da689c313, 2019-01-14)\"> <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=\"namedview10\" showgrid=\"false\" showguides=\"true\" inkscape:guide-bbox=\"true\" inkscape:zoom=\"9.44\" inkscape:cx=\"69.372244\" inkscape:cy=\"85.073455\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:current-layer=\"svg8\"> <sodipodi:guide position=\"50,117.79661\" orientation=\"1,0\" id=\"guide819\" inkscape:locked=\"false\" /> <sodipodi:guide position=\"57.627119,50\" orientation=\"0,1\" id=\"guide821\" inkscape:locked=\"false\" /> </sodipodi:namedview> <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 /> </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,-27.986574,-42.187244)\" 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 50,50 21.042889,9.3993342 C 36.191421,-2.001434 60.726552,-3.6768009 78.8105,9.1490935 Z\" id=\"path821\" inkscape:connector-curvature=\"0\" /> </svg> "
|
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\" id=\"svg8\" version=\"1.1\" viewBox=\"0 0 100 100\" height=\"100\" width=\"100\"> <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\" /> </cc:Work> </rdf:RDF> </metadata> <defs id=\"defs6\"> <linearGradient id=\"linearGradient820\"> <stop style=\"stop-color:#000000;stop-opacity:1;\" offset=\"0\" id=\"innercolor\" /> <stop style=\"stop-color:#000000;stop-opacity:0\" offset=\"1\" id=\"outercolor\" /> </linearGradient> <radialGradient xlink:href=\"#linearGradient820\" id=\"radialGradient828\" cx=\"49.787739\" cy=\"53.828533\" fx=\"49.787739\" fy=\"53.828533\" r=\"28.883806\" gradientTransform=\"matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-27.986574,-42.187244)\" gradientUnits=\"userSpaceOnUse\" /> </defs> <path id=\"path821\" d=\"M 50,50 21.042889,9.3993342 C 36.191421,-2.001434 60.726552,-3.6768009 78.8105,9.1490935 Z\" 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\" /> </svg> "
|
||||||
public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient)
|
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_svg() { return new FixedUiElement(Svg.direction_gradient);}
|
||||||
public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);}
|
public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);}
|
||||||
|
|
22
UI/Base/FeatureSwitched.ts
Normal file
22
UI/Base/FeatureSwitched.ts
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import {UIElement} from "../UIElement";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
|
||||||
|
export default class FeatureSwitched extends UIElement{
|
||||||
|
private readonly _upstream: UIElement;
|
||||||
|
private readonly _swtch: UIEventSource<boolean>;
|
||||||
|
|
||||||
|
constructor(upstream :UIElement,
|
||||||
|
swtch: UIEventSource<boolean>) {
|
||||||
|
super(swtch);
|
||||||
|
this._upstream = upstream;
|
||||||
|
this._swtch = swtch;
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
if(this._swtch.data){
|
||||||
|
return this._upstream.Render();
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,9 +1,9 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {DropDown} from "./Input/DropDown";
|
import {DropDown} from "../Input/DropDown";
|
||||||
import Translations from "./i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import State from "../State";
|
import State from "../../State";
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {BaseLayer} from "../Models/BaseLayer";
|
import {BaseLayer} from "../../Models/BaseLayer";
|
||||||
|
|
||||||
export default class BackgroundSelector extends UIElement {
|
export default class BackgroundSelector extends UIElement {
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import * as L from "leaflet"
|
import * as L from "leaflet"
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import Loc from "../Models/Loc";
|
import Loc from "../../Models/Loc";
|
||||||
import {UIElement} from "./UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {BaseLayer} from "../Models/BaseLayer";
|
import BaseLayer from "../../Models/BaseLayer";
|
||||||
|
|
||||||
export class Basemap {
|
export class Basemap {
|
||||||
|
|
80
UI/BigComponents/FullWelcomePaneWithTabs.ts
Normal file
80
UI/BigComponents/FullWelcomePaneWithTabs.ts
Normal file
|
@ -0,0 +1,80 @@
|
||||||
|
import {UIElement} from "../UIElement";
|
||||||
|
import State from "../../State";
|
||||||
|
import WelcomeMessage from "./WelcomeMessage";
|
||||||
|
import * as personal from "../../assets/themes/personalLayout/personalLayout.json";
|
||||||
|
import PersonalLayersPanel from "./PersonalLayersPanel";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import ShareScreen from "./ShareScreen";
|
||||||
|
import MoreScreen from "./MoreScreen";
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
import Constants from "../../Models/Constants";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
import Locale from "../i18n/Locale";
|
||||||
|
import {TabbedComponent} from "../Base/TabbedComponent";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
|
import UserDetails from "../../Logic/Osm/OsmConnection";
|
||||||
|
|
||||||
|
export default class FullWelcomePaneWithTabs extends UIElement {
|
||||||
|
private readonly _layoutToUse: UIEventSource<LayoutConfig>;
|
||||||
|
private readonly _userDetails: UIEventSource<UserDetails>;
|
||||||
|
|
||||||
|
private readonly _component: UIElement;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(State.state.layoutToUse);
|
||||||
|
this._layoutToUse = State.state.layoutToUse;
|
||||||
|
this._userDetails = State.state.osmConnection.userDetails;
|
||||||
|
|
||||||
|
|
||||||
|
const layoutToUse = this._layoutToUse.data;
|
||||||
|
let welcome: UIElement = new WelcomeMessage();
|
||||||
|
if (layoutToUse.id === personal.id) {
|
||||||
|
welcome = new PersonalLayersPanel();
|
||||||
|
}
|
||||||
|
const tabs = [
|
||||||
|
{header: `<img src='${layoutToUse.icon}'>`, content: welcome},
|
||||||
|
{
|
||||||
|
header: Svg.osm_logo_img,
|
||||||
|
content: Translations.t.general.openStreetMapIntro as UIElement
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
|
|
||||||
|
if (State.state.featureSwitchShareScreen.data) {
|
||||||
|
tabs.push({header: Svg.share_img, content: new ShareScreen()});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.state.featureSwitchMoreQuests.data) {
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
header: Svg.add_img,
|
||||||
|
content: new MoreScreen()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
tabs.push({
|
||||||
|
header: Svg.help,
|
||||||
|
content: new VariableUiElement(this._userDetails.map(userdetails => {
|
||||||
|
if (userdetails.csCount < Constants.userJourney.mapCompleteHelpUnlock) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return new Combine([Translations.t.general.aboutMapcomplete, "<br/>Version " + Constants.vNumber]).Render();
|
||||||
|
}, [Locale.language]))
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
this._component = new TabbedComponent(tabs, State.state.welcomeMessageOpenedTab)
|
||||||
|
.ListenTo(this._userDetails);
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
return this._component.Render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
33
UI/BigComponents/LayerControlPanel.ts
Normal file
33
UI/BigComponents/LayerControlPanel.ts
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
import {UIElement} from "../UIElement";
|
||||||
|
import State from "../../State";
|
||||||
|
import BackgroundSelector from "./BackgroundSelector";
|
||||||
|
import LayerSelection from "./LayerSelection";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
|
||||||
|
export default class LayerControlPanel extends UIElement{
|
||||||
|
private readonly _panel: UIElement;
|
||||||
|
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super();
|
||||||
|
let layerControlPanel: UIElement = undefined;
|
||||||
|
if (State.state.layoutToUse.data.enableBackgroundLayerSelection) {
|
||||||
|
layerControlPanel = new BackgroundSelector();
|
||||||
|
layerControlPanel.SetStyle("margin:1em");
|
||||||
|
layerControlPanel.onClick(() => {
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (State.state.filteredLayers.data.length > 1) {
|
||||||
|
const layerSelection = new LayerSelection();
|
||||||
|
layerSelection.onClick(() => { });
|
||||||
|
layerControlPanel = new Combine([layerSelection, "<br/>", layerControlPanel]);
|
||||||
|
}
|
||||||
|
this._panel = layerControlPanel;
|
||||||
|
}
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
return this._panel.Render();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import CheckBox from "./Input/CheckBox";
|
import {UIElement} from "../UIElement";
|
||||||
import Combine from "./Base/Combine";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import State from "../State";
|
import State from "../../State";
|
||||||
import Translations from "./i18n/Translations";
|
import CheckBox from "../Input/CheckBox";
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
import Combine from "../Base/Combine";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import Translations from "../i18n/Translations";
|
||||||
|
|
||||||
export class LayerSelection extends UIElement {
|
export default class LayerSelection extends UIElement {
|
||||||
|
|
||||||
private readonly _checkboxes: UIElement[];
|
private readonly _checkboxes: UIElement[];
|
||||||
|
|
||||||
|
@ -16,7 +16,9 @@ export class LayerSelection extends UIElement {
|
||||||
this._checkboxes = [];
|
this._checkboxes = [];
|
||||||
|
|
||||||
for (const layer of State.state.filteredLayers.data) {
|
for (const layer of State.state.filteredLayers.data) {
|
||||||
const leafletStyle = layer.layerDef.GenerateLeafletStyle(new UIEventSource<any>({id: "node/-1"}), true)
|
const leafletStyle = layer.layerDef.GenerateLeafletStyle(
|
||||||
|
new UIEventSource<any>({id: "node/-1"}),
|
||||||
|
false)
|
||||||
const leafletHtml = leafletStyle.icon.html;
|
const leafletHtml = leafletStyle.icon.html;
|
||||||
const icon =
|
const icon =
|
||||||
new FixedUiElement(leafletHtml.Render())
|
new FixedUiElement(leafletHtml.Render())
|
|
@ -1,17 +1,17 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||||
import {VerticalCombine} from "./Base/VerticalCombine";
|
import {UIElement} from "../UIElement";
|
||||||
import Translations from "./i18n/Translations";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import Combine from "./Base/Combine";
|
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
import {SubtleButton} from "./Base/SubtleButton";
|
import Svg from "../../Svg";
|
||||||
import State from "../State";
|
import State from "../../State";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import Combine from "../Base/Combine";
|
||||||
import Svg from "../Svg";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
import Translations from "../i18n/Translations";
|
||||||
import * as personal from "../assets/themes/personalLayout/personalLayout.json"
|
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
||||||
import Constants from "../Models/Constants";
|
import Constants from "../../Models/Constants";
|
||||||
|
|
||||||
export class MoreScreen extends UIElement {
|
export default class MoreScreen extends UIElement {
|
||||||
|
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
|
@ -1,17 +1,16 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import State from "../State";
|
import {UIElement} from "../UIElement";
|
||||||
import Translations from "../UI/i18n/Translations";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
|
import {AllKnownLayouts} from "../../Customizations/AllKnownLayouts";
|
||||||
import Combine from "../UI/Base/Combine";
|
import Svg from "../../Svg";
|
||||||
import CheckBox from "../UI/Input/CheckBox";
|
import State from "../../State";
|
||||||
import * as personal from "../assets/themes/personalLayout/personalLayout.json";
|
import Combine from "../Base/Combine";
|
||||||
import {SubtleButton} from "./Base/SubtleButton";
|
import CheckBox from "../Input/CheckBox";
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import Svg from "../Svg";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
import Translations from "../i18n/Translations";
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import * as personal from "../../assets/themes/personalLayout/personalLayout.json"
|
||||||
|
export default class PersonalLayersPanel extends UIElement {
|
||||||
export class PersonalLayersPanel extends UIElement {
|
|
||||||
private checkboxes: UIElement[] = [];
|
private checkboxes: UIElement[] = [];
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
|
@ -1,16 +1,15 @@
|
||||||
import Locale from "./i18n/Locale";
|
import Locale from "../i18n/Locale";
|
||||||
import {UIElement} from "./UIElement";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import {TextField} from "./Input/TextField";
|
import {Translation} from "../i18n/Translation";
|
||||||
import {Geocoding} from "../Logic/Osm/Geocoding";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Translations from "./i18n/Translations";
|
import Svg from "../../Svg";
|
||||||
import State from "../State";
|
import State from "../../State";
|
||||||
|
import {TextField} from "../Input/TextField";
|
||||||
|
import {Geocoding} from "../../Logic/Osm/Geocoding";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
export default class SearchAndGo extends UIElement {
|
||||||
import Svg from "../Svg";
|
|
||||||
import {Translation} from "./i18n/Translation";
|
|
||||||
|
|
||||||
export class SearchAndGo extends UIElement {
|
|
||||||
|
|
||||||
private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
private _placeholder = new UIEventSource<Translation>(Translations.t.general.search.search)
|
||||||
private _searchField = new TextField({
|
private _searchField = new TextField({
|
||||||
|
@ -39,10 +38,16 @@ export class SearchAndGo extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InnerRender(): string {
|
||||||
|
return this._searchField.Render() +
|
||||||
|
this._goButton.Render();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// Triggered by 'enter' or onclick
|
// Triggered by 'enter' or onclick
|
||||||
private RunSearch() {
|
private RunSearch() {
|
||||||
const searchString = this._searchField.GetValue().data;
|
const searchString = this._searchField.GetValue().data;
|
||||||
if(searchString === undefined || searchString === ""){
|
if (searchString === undefined || searchString === "") {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this._searchField.GetValue().setData("");
|
this._searchField.GetValue().setData("");
|
||||||
|
@ -57,7 +62,7 @@ export class SearchAndGo extends UIElement {
|
||||||
}
|
}
|
||||||
|
|
||||||
const bb = result[0].boundingbox;
|
const bb = result[0].boundingbox;
|
||||||
const bounds : [[number, number], [number, number]] = [
|
const bounds: [[number, number], [number, number]] = [
|
||||||
[bb[0], bb[2]],
|
[bb[0], bb[2]],
|
||||||
[bb[1], bb[3]]
|
[bb[1], bb[3]]
|
||||||
]
|
]
|
||||||
|
@ -71,11 +76,5 @@ export class SearchAndGo extends UIElement {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
InnerRender(): string {
|
|
||||||
return this._searchField.Render() +
|
|
||||||
this._goButton.Render();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -1,4 +1,4 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
|
|
||||||
export default class ShareButton extends UIElement{
|
export default class ShareButton extends UIElement{
|
||||||
private _embedded: UIElement;
|
private _embedded: UIElement;
|
|
@ -1,21 +1,21 @@
|
||||||
import {UIElement} from "./UIElement";
|
import {VerticalCombine} from "../Base/VerticalCombine";
|
||||||
import Translations from "./i18n/Translations";
|
import {UIElement} from "../UIElement";
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
import Combine from "./Base/Combine";
|
import {Translation} from "../i18n/Translation";
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
import LayoutConfig from "../../Customizations/JSON/LayoutConfig";
|
||||||
import CheckBox from "./Input/CheckBox";
|
import Svg from "../../Svg";
|
||||||
import {VerticalCombine} from "./Base/VerticalCombine";
|
import Combine from "../Base/Combine";
|
||||||
import State from "../State";
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
import {FilteredLayer} from "../Logic/FilteredLayer";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {Utils} from "../Utils";
|
import {Utils} from "../../Utils";
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
import State from "../../State";
|
||||||
import {SubtleButton} from "./Base/SubtleButton";
|
import CheckBox from "../Input/CheckBox";
|
||||||
import Svg from "../Svg";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
import {Translation} from "./i18n/Translation";
|
import Translations from "../i18n/Translations";
|
||||||
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
|
import Constants from "../../Models/Constants";
|
||||||
import Constants from "../Models/Constants";
|
import LayerConfig from "../../Customizations/JSON/LayerConfig";
|
||||||
|
|
||||||
export class ShareScreen extends UIElement {
|
export default class ShareScreen extends UIElement {
|
||||||
private readonly _options: UIElement;
|
private readonly _options: UIElement;
|
||||||
private readonly _iframeCode: UIElement;
|
private readonly _iframeCode: UIElement;
|
||||||
public iframe: UIEventSource<string>;
|
public iframe: UIEventSource<string>;
|
||||||
|
@ -61,7 +61,7 @@ export class ShareScreen extends UIElement {
|
||||||
}, [currentLocation]));
|
}, [currentLocation]));
|
||||||
|
|
||||||
|
|
||||||
function fLayerToParam(flayer: FilteredLayer) {
|
function fLayerToParam(flayer: {isDisplayed: UIEventSource<boolean>, layerDef: LayerConfig}) {
|
||||||
if (flayer.isDisplayed.data) {
|
if (flayer.isDisplayed.data) {
|
||||||
return null; // Being displayed is the default
|
return null; // Being displayed is the default
|
||||||
}
|
}
|
|
@ -1,20 +1,19 @@
|
||||||
import {UIElement} from "./UIElement";
|
|
||||||
import {Tag, TagUtils} from "../Logic/Tags";
|
|
||||||
import Translations from "./i18n/Translations";
|
|
||||||
import Combine from "./Base/Combine";
|
|
||||||
import {SubtleButton} from "./Base/SubtleButton";
|
|
||||||
import Locale from "./i18n/Locale";
|
|
||||||
import State from "../State";
|
|
||||||
|
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
|
||||||
import Svg from "../Svg";
|
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
|
||||||
import Constants from "../Models/Constants";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
* Asks to add a feature at the last clicked location, at least if zoom is sufficient
|
||||||
*/
|
*/
|
||||||
export class SimpleAddUI extends UIElement {
|
import Locale from "../i18n/Locale";
|
||||||
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import {Tag, TagUtils} from "../../Logic/Tags";
|
||||||
|
import {UIElement} from "../UIElement";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import {SubtleButton} from "../Base/SubtleButton";
|
||||||
|
import State from "../../State";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import Constants from "../../Models/Constants";
|
||||||
|
|
||||||
|
export default class SimpleAddUI extends UIElement {
|
||||||
private readonly _addButtons: UIElement[];
|
private readonly _addButtons: UIElement[];
|
||||||
|
|
||||||
private _loginButton : UIElement;
|
private _loginButton : UIElement;
|
|
@ -1,25 +1,25 @@
|
||||||
import {UIElement} from "./UIElement";
|
|
||||||
import {FixedUiElement} from "./Base/FixedUiElement";
|
|
||||||
import {VariableUiElement} from "./Base/VariableUIElement";
|
|
||||||
import Translations from "./i18n/Translations";
|
|
||||||
import {UserDetails} from "../Logic/Osm/OsmConnection";
|
|
||||||
import State from "../State";
|
|
||||||
import {UIEventSource} from "../Logic/UIEventSource";
|
|
||||||
import Combine from "./Base/Combine";
|
|
||||||
import Svg from "../Svg";
|
|
||||||
import Link from "./Base/Link";
|
|
||||||
import LanguagePicker from "./LanguagePicker";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles and updates the user badge
|
* Handles and updates the user badge
|
||||||
*/
|
*/
|
||||||
export class UserBadge extends UIElement {
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
|
import {UIElement} from "../UIElement";
|
||||||
|
import {VariableUiElement} from "../Base/VariableUIElement";
|
||||||
|
import {UserDetails} from "../../Logic/Osm/OsmConnection";
|
||||||
|
import Svg from "../../Svg";
|
||||||
|
import State from "../../State";
|
||||||
|
import Combine from "../Base/Combine";
|
||||||
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
import LanguagePicker from "../LanguagePicker";
|
||||||
|
import Translations from "../i18n/Translations";
|
||||||
|
import Link from "../Base/Link";
|
||||||
|
|
||||||
|
export default class UserBadge extends UIElement {
|
||||||
private _userDetails: UIEventSource<UserDetails>;
|
private _userDetails: UIEventSource<UserDetails>;
|
||||||
private _logout: UIElement;
|
private _logout: UIElement;
|
||||||
private _homeButton: UIElement;
|
private _homeButton: UIElement;
|
||||||
private _languagePicker: UIElement;
|
private _languagePicker: UIElement;
|
||||||
|
|
||||||
private _loginButton : UIElement;
|
private _loginButton: UIElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super(State.state.osmConnection.userDetails);
|
super(State.state.osmConnection.userDetails);
|
|
@ -1,12 +1,12 @@
|
||||||
import {UIElement} from "./UIElement";
|
import Locale from "../i18n/Locale";
|
||||||
import Locale from "../UI/i18n/Locale";
|
import {UIElement} from "../UIElement";
|
||||||
import State from "../State";
|
import State from "../../State";
|
||||||
import Translations from "./i18n/Translations";
|
import Combine from "../Base/Combine";
|
||||||
import Combine from "./Base/Combine";
|
import LanguagePicker from "../LanguagePicker";
|
||||||
import LanguagePicker from "./LanguagePicker";
|
import Translations from "../i18n/Translations";
|
||||||
|
|
||||||
|
|
||||||
export class WelcomeMessage extends UIElement {
|
export default class WelcomeMessage extends UIElement {
|
||||||
private languagePicker: UIElement;
|
private languagePicker: UIElement;
|
||||||
|
|
||||||
private readonly description: UIElement;
|
private readonly description: UIElement;
|
||||||
|
@ -25,11 +25,6 @@ export class WelcomeMessage extends UIElement {
|
||||||
"<h3>", layout.title, "</h3>",
|
"<h3>", layout.title, "</h3>",
|
||||||
layout.description
|
layout.description
|
||||||
])
|
])
|
||||||
layout.descriptionTail
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
this.plzLogIn =
|
this.plzLogIn =
|
||||||
Translations.t.general.loginWithOpenStreetMap
|
Translations.t.general.loginWithOpenStreetMap
|
||||||
.onClick(() => {
|
.onClick(() => {
|
|
@ -2,10 +2,9 @@ import {UIElement} from "./UIElement";
|
||||||
import Translations from "./i18n/Translations";
|
import Translations from "./i18n/Translations";
|
||||||
import State from "../State";
|
import State from "../State";
|
||||||
|
|
||||||
export class CenterMessageBox extends UIElement {
|
export default class CenterMessageBox extends UIElement {
|
||||||
|
|
||||||
constructor(
|
constructor() {
|
||||||
) {
|
|
||||||
super(State.state.centerMessage);
|
super(State.state.centerMessage);
|
||||||
|
|
||||||
this.ListenTo(State.state.locationControl);
|
this.ListenTo(State.state.locationControl);
|
||||||
|
@ -19,8 +18,11 @@ export class CenterMessageBox extends UIElement {
|
||||||
return {innerHtml: State.state.centerMessage.data, done: false};
|
return {innerHtml: State.state.centerMessage.data, done: false};
|
||||||
}
|
}
|
||||||
const lu = State.state.layerUpdater;
|
const lu = State.state.layerUpdater;
|
||||||
if(lu.retries.data > 0) {
|
if (lu.retries.data > 0) {
|
||||||
return {innerHtml: Translations.t.centerMessage.retrying.Subs({count: ""+ lu.retries.data}).Render(), done: false};
|
return {
|
||||||
|
innerHtml: Translations.t.centerMessage.retrying.Subs({count: "" + lu.retries.data}).Render(),
|
||||||
|
done: false
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lu.runningQuery.data) {
|
if (lu.runningQuery.data) {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import Combine from "./Base/Combine";
|
||||||
/**
|
/**
|
||||||
* Handles the full screen popup on mobile
|
* Handles the full screen popup on mobile
|
||||||
*/
|
*/
|
||||||
export class FullScreenMessageBox extends UIElement {
|
export default class FullScreenMessageBox extends UIElement {
|
||||||
|
|
||||||
private readonly returnToTheMap: UIElement;
|
private readonly returnToTheMap: UIElement;
|
||||||
private _content: UIElement;
|
private _content: UIElement;
|
||||||
|
|
|
@ -9,8 +9,8 @@ import State from "../../State";
|
||||||
import Svg from "../../Svg";
|
import Svg from "../../Svg";
|
||||||
|
|
||||||
export default class EditableTagRendering extends UIElement {
|
export default class EditableTagRendering extends UIElement {
|
||||||
private _tags: UIEventSource<any>;
|
private readonly _tags: UIEventSource<any>;
|
||||||
private _configuration: TagRenderingConfig;
|
private readonly _configuration: TagRenderingConfig;
|
||||||
|
|
||||||
private _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
private _editMode: UIEventSource<boolean> = new UIEventSource<boolean>(false);
|
||||||
private _editButton: UIElement;
|
private _editButton: UIElement;
|
||||||
|
|
|
@ -8,14 +8,14 @@ import TagRenderingAnswer from "./TagRenderingAnswer";
|
||||||
import State from "../../State";
|
import State from "../../State";
|
||||||
import {FixedUiElement} from "../Base/FixedUiElement";
|
import {FixedUiElement} from "../Base/FixedUiElement";
|
||||||
|
|
||||||
export class FeatureInfoBox extends UIElement {
|
export default class FeatureInfoBox extends UIElement {
|
||||||
private _tags: UIEventSource<any>;
|
private _tags: UIEventSource<any>;
|
||||||
private _layerConfig: LayerConfig;
|
private _layerConfig: LayerConfig;
|
||||||
|
|
||||||
private _title : UIElement;
|
private _title: UIElement;
|
||||||
private _titleIcons: UIElement;
|
private _titleIcons: UIElement;
|
||||||
private _renderings: UIElement[];
|
private _renderings: UIElement[];
|
||||||
private _questionBox : UIElement;
|
private _questionBox: UIElement;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
tags: UIEventSource<any>,
|
tags: UIEventSource<any>,
|
||||||
|
@ -36,14 +36,14 @@ export class FeatureInfoBox extends UIElement {
|
||||||
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))
|
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))
|
||||||
.SetClass("featureinfobox-icons");
|
.SetClass("featureinfobox-icons");
|
||||||
|
|
||||||
let questionBox : UIElement = undefined;
|
let questionBox: UIElement = undefined;
|
||||||
if (State.state.featureSwitchUserbadge.data) {
|
if (State.state.featureSwitchUserbadge.data) {
|
||||||
questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
|
questionBox = new QuestionBox(tags, layerConfig.tagRenderings);
|
||||||
}
|
}
|
||||||
|
|
||||||
let questionBoxIsUsed = false;
|
let questionBoxIsUsed = false;
|
||||||
this._renderings = layerConfig.tagRenderings.map(tr => {
|
this._renderings = layerConfig.tagRenderings.map(tr => {
|
||||||
if(tr.question === null){
|
if (tr.question === null) {
|
||||||
questionBoxIsUsed = true;
|
questionBoxIsUsed = true;
|
||||||
// This is the question box!
|
// This is the question box!
|
||||||
return questionBox;
|
return questionBox;
|
||||||
|
@ -51,7 +51,7 @@ export class FeatureInfoBox extends UIElement {
|
||||||
return new EditableTagRendering(tags, tr);
|
return new EditableTagRendering(tags, tr);
|
||||||
});
|
});
|
||||||
this._renderings[0]?.SetClass("first-rendering");
|
this._renderings[0]?.SetClass("first-rendering");
|
||||||
if(!questionBoxIsUsed){
|
if (!questionBoxIsUsed) {
|
||||||
this._renderings.push(questionBox);
|
this._renderings.push(questionBox);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,9 +9,9 @@ import Translations from "../i18n/Translations";
|
||||||
* Generates all the questions, one by one
|
* Generates all the questions, one by one
|
||||||
*/
|
*/
|
||||||
export default class QuestionBox extends UIElement {
|
export default class QuestionBox extends UIElement {
|
||||||
private _tags: UIEventSource<any>;
|
private readonly _tags: UIEventSource<any>;
|
||||||
|
|
||||||
private _tagRenderings: TagRenderingConfig[];
|
private readonly _tagRenderings: TagRenderingConfig[];
|
||||||
private _tagRenderingQuestions: UIElement[];
|
private _tagRenderingQuestions: UIElement[];
|
||||||
|
|
||||||
private _skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
private _skippedQuestions: UIEventSource<number[]> = new UIEventSource<number[]>([])
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import {UIEventSource} from "../../Logic/UIEventSource";
|
import {UIEventSource} from "../../Logic/UIEventSource";
|
||||||
import {UIElement} from "../UIElement";
|
import {UIElement} from "../UIElement";
|
||||||
import Translations from "../i18n/Translations";
|
import Translations from "../i18n/Translations";
|
||||||
import {OsmConnection, UserDetails} from "../../Logic/Osm/OsmConnection";
|
import UserDetails, {OsmConnection} from "../../Logic/Osm/OsmConnection";
|
||||||
|
|
||||||
export class SaveButton extends UIElement {
|
export class SaveButton extends UIElement {
|
||||||
|
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {SubstitutedTranslation} from "../SpecialVisualizations";
|
||||||
* Displays the correct value for a known tagrendering
|
* Displays the correct value for a known tagrendering
|
||||||
*/
|
*/
|
||||||
export default class TagRenderingAnswer extends UIElement {
|
export default class TagRenderingAnswer extends UIElement {
|
||||||
private _tags: UIEventSource<any>;
|
private readonly _tags: UIEventSource<any>;
|
||||||
private _configuration: TagRenderingConfig;
|
private _configuration: TagRenderingConfig;
|
||||||
private _content: UIElement;
|
private _content: UIElement;
|
||||||
|
|
||||||
|
|
|
@ -25,7 +25,7 @@ import Constants from "../../Models/Constants";
|
||||||
* Note that the value _migh_ already be known, e.g. when selected or when changing the value
|
* Note that the value _migh_ already be known, e.g. when selected or when changing the value
|
||||||
*/
|
*/
|
||||||
export default class TagRenderingQuestion extends UIElement {
|
export default class TagRenderingQuestion extends UIElement {
|
||||||
private _tags: UIEventSource<any>;
|
private readonly _tags: UIEventSource<any>;
|
||||||
private _configuration: TagRenderingConfig;
|
private _configuration: TagRenderingConfig;
|
||||||
|
|
||||||
private _saveButton: UIElement;
|
private _saveButton: UIElement;
|
||||||
|
|
141
UI/ShowDataLayer.ts
Normal file
141
UI/ShowDataLayer.ts
Normal file
|
@ -0,0 +1,141 @@
|
||||||
|
/**
|
||||||
|
* The data layer shows all the given geojson elements with the appropriate icon etc
|
||||||
|
*/
|
||||||
|
import {UIEventSource} from "../Logic/UIEventSource";
|
||||||
|
import * as L from "leaflet"
|
||||||
|
import LayerConfig from "../Customizations/JSON/LayerConfig";
|
||||||
|
import State from "../State";
|
||||||
|
import LazyElement from "./Base/LazyElement";
|
||||||
|
import Hash from "../Logic/Web/Hash";
|
||||||
|
import {GeoOperations} from "../Logic/GeoOperations";
|
||||||
|
import FeatureInfoBox from "./Popup/FeatureInfoBox";
|
||||||
|
|
||||||
|
export default class ShowDataLayer {
|
||||||
|
|
||||||
|
private readonly _layerDict;
|
||||||
|
private readonly _leafletMap: UIEventSource<L.Map>;
|
||||||
|
|
||||||
|
constructor(features: UIEventSource<{ feature: any, freshness: Date }[]>,
|
||||||
|
leafletMap: UIEventSource<L.Map>,
|
||||||
|
layers: { layerDef: LayerConfig, isDisplayed: UIEventSource<boolean> }[]) {
|
||||||
|
this._leafletMap = leafletMap;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
let oldGeoLayer: L.Layer = undefined;
|
||||||
|
|
||||||
|
this._layerDict = {};
|
||||||
|
for (const layer of layers) {
|
||||||
|
this._layerDict[layer.layerDef.id] = layer.layerDef;
|
||||||
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
if (features.data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (leafletMap.data === undefined) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const mp = leafletMap.data;
|
||||||
|
|
||||||
|
const feats = features.data.map(ff => ff.feature);
|
||||||
|
const geoLayer = self.CreateGeojsonLayer(feats);
|
||||||
|
|
||||||
|
if (oldGeoLayer) {
|
||||||
|
mp.removeLayer(oldGeoLayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
geoLayer.addTo(mp);
|
||||||
|
oldGeoLayer = geoLayer;
|
||||||
|
}
|
||||||
|
|
||||||
|
features.addCallbackAndRun(() => update());
|
||||||
|
leafletMap.addCallback(() => update());
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private createStyleFor(feature) {
|
||||||
|
const tagsSource = State.state.allElements.getEventSourceFor(feature);
|
||||||
|
// Every object is tied to exactly one layer
|
||||||
|
const layer = this._layerDict[feature._matching_layer_id];
|
||||||
|
return layer.GenerateLeafletStyle(tagsSource, layer._showOnPopup !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
private pointToLayer(feature, latLng): L.Layer {
|
||||||
|
// Leaflet cannot handle geojson points natively
|
||||||
|
// We have to convert them to the appropriate icon
|
||||||
|
// Click handling is done in the next step
|
||||||
|
|
||||||
|
const tagSource = State.state.allElements.getEventSourceFor(feature);
|
||||||
|
const layer : LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||||
|
|
||||||
|
const style = layer.GenerateLeafletStyle(tagSource, !(layer.title === undefined && (layer.tagRenderings ?? []).length === 0));
|
||||||
|
return L.marker(latLng, {
|
||||||
|
icon: L.divIcon({
|
||||||
|
html: style.icon.html.Render(),
|
||||||
|
className: style.icon.className,
|
||||||
|
iconAnchor: style.icon.iconAnchor,
|
||||||
|
iconUrl: style.icon.iconUrl,
|
||||||
|
popupAnchor: style.icon.popupAnchor,
|
||||||
|
iconSize: style.icon.iconSize
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private postProcessFeature(feature, leafletLayer: L.Layer){
|
||||||
|
const layer : LayerConfig = this._layerDict[feature._matching_layer_id];
|
||||||
|
if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
|
||||||
|
// No popup action defined -> Don't do anything
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popup = L.popup({
|
||||||
|
autoPan: true,
|
||||||
|
closeOnEscapeKey: true,
|
||||||
|
}, leafletLayer);
|
||||||
|
|
||||||
|
|
||||||
|
const tags = State.state.allElements.getEventSourceFor(feature);
|
||||||
|
let uiElement: LazyElement = new LazyElement(() => new FeatureInfoBox(tags, layer));
|
||||||
|
popup.setContent(uiElement.Render());
|
||||||
|
leafletLayer.bindPopup(popup);
|
||||||
|
// We first render the UIelement (which'll still need an update later on...)
|
||||||
|
// But at least it'll be visible already
|
||||||
|
|
||||||
|
|
||||||
|
leafletLayer.on("click", (e) => {
|
||||||
|
// We set the element as selected...
|
||||||
|
uiElement.Activate();
|
||||||
|
State.state.selectedElement.setData(feature);
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
|
||||||
|
// This element is in the URL, so this is a share link
|
||||||
|
// We already open it
|
||||||
|
uiElement.Activate();
|
||||||
|
popup.setContent(uiElement.Render());
|
||||||
|
|
||||||
|
const center = GeoOperations.centerpoint(feature).geometry.coordinates;
|
||||||
|
popup.setLatLng({lat: center[1], lng: center[0]});
|
||||||
|
popup.openOn(State.state.leafletMap.data);
|
||||||
|
State.state.selectedElement.setData(feature);
|
||||||
|
uiElement.Update();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private CreateGeojsonLayer(features: any[]): L.Layer {
|
||||||
|
const self = this;
|
||||||
|
const data = {
|
||||||
|
type: "FeatureCollection",
|
||||||
|
features: features
|
||||||
|
}
|
||||||
|
return L.geoJSON(data, {
|
||||||
|
style: feature => self.createStyleFor(feature),
|
||||||
|
pointToLayer: (feature, latLng) => self.pointToLayer(feature, latLng),
|
||||||
|
onEachFeature: (feature, leafletLayer) => self.postProcessFeature(feature, leafletLayer)
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9,7 +9,7 @@ import Locale from "../UI/i18n/Locale";
|
||||||
import {ImageUploadFlow} from "./Image/ImageUploadFlow";
|
import {ImageUploadFlow} from "./Image/ImageUploadFlow";
|
||||||
import {Translation} from "./i18n/Translation";
|
import {Translation} from "./i18n/Translation";
|
||||||
|
|
||||||
import ShareButton from "./ShareButton";
|
import ShareButton from "./BigComponents/ShareButton";
|
||||||
import Svg from "../Svg";
|
import Svg from "../Svg";
|
||||||
import ReviewElement from "./Reviews/ReviewElement";
|
import ReviewElement from "./Reviews/ReviewElement";
|
||||||
import MangroveReviews from "../Logic/Web/MangroveReviews";
|
import MangroveReviews from "../Logic/Web/MangroveReviews";
|
||||||
|
|
|
@ -5,7 +5,10 @@
|
||||||
},
|
},
|
||||||
"minzoom": 16,
|
"minzoom": 16,
|
||||||
"overpassTags": {
|
"overpassTags": {
|
||||||
"or": ["camera:direction~*","direction~*"]
|
"or": [
|
||||||
|
"camera:direction~*",
|
||||||
|
"direction~*"
|
||||||
|
]
|
||||||
},
|
},
|
||||||
"doNotDownload": true,
|
"doNotDownload": true,
|
||||||
"passAllFeatures": true,
|
"passAllFeatures": true,
|
||||||
|
@ -14,7 +17,16 @@
|
||||||
"en": "This layer visualizes directions"
|
"en": "This layer visualizes directions"
|
||||||
},
|
},
|
||||||
"tagRenderings": [],
|
"tagRenderings": [],
|
||||||
"icon": "./assets/svg/direction_gradient.svg",
|
"icon": {
|
||||||
|
"render": "direction_gradient:var(--catch-detail-color)",
|
||||||
|
"#": "For some weird reason, showing the icon in the layer control panel breaks the svg-gradient (because the svg gradient has a global color or smthng) - so we use a different icon without gradient",
|
||||||
|
"mappings": [
|
||||||
|
{
|
||||||
|
"if": "id=node/-1",
|
||||||
|
"then": "direction:var(--catch-detail-color)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
"rotation": {
|
"rotation": {
|
||||||
"render": "{camera:direction}deg",
|
"render": "{camera:direction}deg",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
|
|
|
@ -335,6 +335,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"rotation": {
|
"rotation": {
|
||||||
|
"#": "Note: {camera:direction} is substituted by a number, giving the string 'calc(123deg + 90deg)' ; it is this string that is used as css property, which interprets the calc",
|
||||||
"render": "calc({camera:direction}deg + 90deg)",
|
"render": "calc({camera:direction}deg + 90deg)",
|
||||||
"mappings": [
|
"mappings": [
|
||||||
{
|
{
|
||||||
|
|
|
@ -5,11 +5,11 @@
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
id="svg8"
|
width="100"
|
||||||
version="1.1"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
height="100"
|
height="100"
|
||||||
width="100">
|
viewBox="0 0 100 100"
|
||||||
|
version="1.1"
|
||||||
|
id="svg8">
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata8">
|
id="metadata8">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
<defs
|
<defs
|
||||||
id="defs6" />
|
id="defs6" />
|
||||||
<path
|
<path
|
||||||
id="path821"
|
style="fill:#000000;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"
|
d="M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z"
|
||||||
style="fill:#000000;fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
|
id="path821" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 1,017 B After Width: | Height: | Size: 1,017 B |
|
@ -6,48 +6,11 @@
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
xmlns:svg="http://www.w3.org/2000/svg"
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
version="1.1"
|
|
||||||
id="svg8"
|
id="svg8"
|
||||||
sodipodi:docname="direction_gradient.svg"
|
version="1.1"
|
||||||
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
|
viewBox="0 0 100 100"
|
||||||
<sodipodi:namedview
|
height="100"
|
||||||
pagecolor="#ffffff"
|
width="100">
|
||||||
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="namedview10"
|
|
||||||
showgrid="false"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:guide-bbox="true"
|
|
||||||
inkscape:zoom="9.44"
|
|
||||||
inkscape:cx="69.372244"
|
|
||||||
inkscape:cy="85.073455"
|
|
||||||
inkscape:window-x="0"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg8">
|
|
||||||
<sodipodi:guide
|
|
||||||
position="50,117.79661"
|
|
||||||
orientation="1,0"
|
|
||||||
id="guide819"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="57.627119,50"
|
|
||||||
orientation="0,1"
|
|
||||||
id="guide821"
|
|
||||||
inkscape:locked="false" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<metadata
|
<metadata
|
||||||
id="metadata8">
|
id="metadata8">
|
||||||
<rdf:RDF>
|
<rdf:RDF>
|
||||||
|
@ -56,7 +19,6 @@
|
||||||
<dc:format>image/svg+xml</dc:format>
|
<dc:format>image/svg+xml</dc:format>
|
||||||
<dc:type
|
<dc:type
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
</cc:Work>
|
||||||
</rdf:RDF>
|
</rdf:RDF>
|
||||||
</metadata>
|
</metadata>
|
||||||
|
@ -65,28 +27,27 @@
|
||||||
<linearGradient
|
<linearGradient
|
||||||
id="linearGradient820">
|
id="linearGradient820">
|
||||||
<stop
|
<stop
|
||||||
id="innercolor"
|
style="stop-color:#000000;stop-opacity:1;"
|
||||||
offset="0"
|
offset="0"
|
||||||
style="stop-color:#000000;stop-opacity:1;" />
|
id="innercolor" />
|
||||||
<stop
|
<stop
|
||||||
id="outercolor"
|
style="stop-color:#000000;stop-opacity:0"
|
||||||
offset="1"
|
offset="1"
|
||||||
style="stop-color:#000000;stop-opacity:0" />
|
id="outercolor" />
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<radialGradient
|
<radialGradient
|
||||||
gradientUnits="userSpaceOnUse"
|
xlink:href="#linearGradient820"
|
||||||
gradientTransform="matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-27.986574,-42.187244)"
|
|
||||||
r="28.883806"
|
|
||||||
fy="53.828533"
|
|
||||||
fx="49.787739"
|
|
||||||
cy="53.828533"
|
|
||||||
cx="49.787739"
|
|
||||||
id="radialGradient828"
|
id="radialGradient828"
|
||||||
xlink:href="#linearGradient820" />
|
cx="49.787739"
|
||||||
|
cy="53.828533"
|
||||||
|
fx="49.787739"
|
||||||
|
fy="53.828533"
|
||||||
|
r="28.883806"
|
||||||
|
gradientTransform="matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-27.986574,-42.187244)"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
</defs>
|
</defs>
|
||||||
<path
|
<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 50,50 21.042889,9.3993342 C 36.191421,-2.001434 60.726552,-3.6768009 78.8105,9.1490935 Z"
|
|
||||||
id="path821"
|
id="path821"
|
||||||
inkscape:connector-curvature="0" />
|
d="M 50,50 21.042889,9.3993342 C 36.191421,-2.001434 60.726552,-3.6768009 78.8105,9.1490935 Z"
|
||||||
|
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" />
|
||||||
</svg>
|
</svg>
|
||||||
|
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 1.6 KiB |
Loading…
Reference in a new issue