I should have commited sooner...

This commit is contained in:
Pieter Vander Vennet 2020-11-17 02:22:48 +01:00
parent 2685b6e734
commit 16612b10ef
35 changed files with 570 additions and 177 deletions

View file

@ -8,23 +8,33 @@ import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
import {Translation} from "../../UI/i18n/Translation";
import {Img} from "../../UI/Img";
import Svg from "../../Svg";
import {SubstitutedTranslation} from "../../UI/SpecialVisualizations";
import {Utils} from "../../Utils";
import Combine from "../../UI/Base/Combine";
import {Browser} from "leaflet";
export default class LayerConfig {
id: string;
name: Translation
description: Translation;
overpassTags: TagsFilter;
doNotDownload: boolean;
passAllFeatures: boolean;
minzoom: number;
title: TagRenderingConfig;
title?: TagRenderingConfig;
titleIcons: TagRenderingConfig[];
icon: TagRenderingConfig;
iconSize: TagRenderingConfig;
rotation: TagRenderingConfig;
color: TagRenderingConfig;
width: TagRenderingConfig;
dashArray: TagRenderingConfig;
@ -53,10 +63,11 @@ export default class LayerConfig {
this.name = Translations.T(json.name);
this.description = Translations.T(json.name);
this.overpassTags = FromJSON.Tag(json.overpassTags, context + ".overpasstags");
this.doNotDownload = json.doNotDownload ?? false,
this.passAllFeatures = json.passAllFeatures ?? false;
this.minzoom = json.minzoom;
this.wayHandling = json.wayHandling ?? 0;
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
this.title = new TagRenderingConfig(json.title);
this.presets = (json.presets ?? []).map(pr =>
({
title: Translations.T(pr.title),
@ -93,7 +104,10 @@ export default class LayerConfig {
function tr(key, deflt) {
const v = json[key];
if (v === undefined) {
if (v === undefined || v === null) {
if (deflt === undefined) {
return undefined;
}
return new TagRenderingConfig(deflt);
}
if (typeof v === "string") {
@ -107,11 +121,19 @@ export default class LayerConfig {
}
this.title = tr("title", "");
this.title = tr("title", undefined);
this.icon = tr("icon", Img.AsData(Svg.bug));
const iconPath = this.icon.GetRenderValue({id: "node/-1"}).txt;
if (iconPath.startsWith(Utils.assets_path)) {
const iconKey = iconPath.substr(Utils.assets_path.length);
if (Svg.All[iconKey] === undefined) {
throw "Builtin SVG asset not found: " + iconPath
}
}
this.iconSize = tr("iconSize", "40,40,center");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");
this.rotation = tr("rotation", "0");
this.dashArray = tr("dashArray", "");
@ -121,13 +143,16 @@ export default class LayerConfig {
public GenerateLeafletStyle(tags: any):
{
color: string;
icon: { popupAnchor: [number, number]; iconAnchor: [number, number]; iconSize: [number, number]; iconUrl: string }; weight: number; dashArray: number[]
icon: {
iconUrl: string,
popupAnchor: [number, number];
iconAnchor: [number, number];
iconSize: [number, number];
html: string;
rotation: number;
};
weight: number; dashArray: number[]
} {
const iconUrl = this.icon?.GetRenderValue(tags)?.txt;
const iconSize = (this.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(",");
const dashArray = this.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number);
function num(str, deflt = 40) {
const n = Number(str);
@ -137,6 +162,33 @@ export default class LayerConfig {
return n;
}
function rendernum(tr: TagRenderingConfig, deflt: number) {
const str = Number(render(tr, "" + deflt));
const n = Number(str);
if (isNaN(n)) {
return deflt;
}
return n;
}
function render(tr: TagRenderingConfig, deflt?: string) {
const str = (tr?.GetRenderValue(tags)?.txt ?? deflt);
return SubstitutedTranslation.SubstituteKeys(str, tags);
}
const iconUrl = render(this.icon);
const iconSize = render(this.iconSize, "40,40,center").split(",");
const dashArray = render(this.dashArray).split(" ").map(Number);
let color = render(this.color, "#00f");
if (color.startsWith("--")) {
color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
}
const weight = rendernum(this.width, 5);
const rotation = rendernum(this.rotation, 0);
const iconW = num(iconSize[0]);
const iconH = num(iconSize[1]);
const mode = iconSize[2] ?? "center"
@ -157,16 +209,22 @@ export default class LayerConfig {
anchorH = iconH;
}
const color = this.color?.GetRenderValue(tags)?.txt ?? "#00f";
let weight = num(this.width?.GetRenderValue(tags)?.txt, 5);
let html = `<img src="${iconUrl}" style="width:100%;height:100%;rotate:${rotation}deg;display:block;" />`;
if (iconUrl.startsWith(Utils.assets_path)) {
const key = iconUrl.substr(Utils.assets_path.length);
html = new Combine([
(Svg.All[key] as string).replace(/stop-color:#000000/g, 'stop-color:' + color)
]).SetStyle(`width:100%;height:100%;rotate:${rotation}deg;display:block;`).Render();
}
return {
icon:
{
iconUrl: iconUrl,
html: html,
iconSize: [iconW, iconH],
iconAnchor: [anchorW, anchorH],
popupAnchor: [0, 3 - anchorH]
popupAnchor: [0, 3 - anchorH],
rotation: rotation,
iconUrl: iconUrl
},
color: color,
weight: weight,

View file

@ -29,6 +29,12 @@ export interface LayerConfigJson {
*/
overpassTags: AndOrTagConfigJson | string;
/**
* If set, this layer will not query overpass; but it'll still match the tags above which are by chance returned by other layers.
* Works well together with 'passAllFeatures', to add decoration
*/
doNotDownload?: boolean;
/**
* The zoomlevel at which point the data is shown and loaded.
*/
@ -39,8 +45,13 @@ export interface LayerConfigJson {
/**
* The title shown in a popup for elements of this layer.
*/
title: string | TagRenderingConfigJson;
title?: string | TagRenderingConfigJson;
/**
* Small icons shown next to the title.
* If not specified, the OsmLink and wikipedia links will be used by default.
* Use an empty array to hide them
*/
titleIcons?: (string | TagRenderingConfigJson)[];
/**
@ -54,9 +65,14 @@ export interface LayerConfigJson {
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
/**
* The rotation of an icon, useful for e.g. directions
*/
rotation?: string | TagRenderingConfigJson;
/**
* The color for way-elements
* The color for way-elements and SVG-elements.
* If the value starts with "--", the style of the body element will be queried for the corresponding variable instead
*/
color?: string | TagRenderingConfigJson;
/**
@ -87,6 +103,11 @@ export interface LayerConfigJson {
*/
hideUnderlayingFeaturesMinPercentage?:number;
/**
* If set, this layer will pass all the features it receives onto the next layer
*/
passAllFeatures?:boolean
/**
* Presets for this layer
*/
@ -98,6 +119,7 @@ export interface LayerConfigJson {
/**
* All the tag renderings.
* A tag rendering is a block that either shows the known value or asks a question.
*/
tagRenderings?: (string | TagRenderingConfigJson) []
}

View file

@ -102,7 +102,19 @@ export interface LayoutConfigJson {
/**
* The layers to display
* The layers to display.
*
* Every layer contains a description of which feature to display - the overpassTags which are queried.
* Instead of running one query for every layer, the query is fused.
*
* Afterwards, every layer is given the list of features.
* Every layer takes away the features that match with them*, and give the leftovers to the next layers.
*
* This implies that the _order_ of the layers is important in the case of features with the same tags;
* as the later layers might never receive their feature.
*
* *layers can also remove 'leftover'-features if the leftovers overlap with a feature in the layer itself
*
*/
layers: (LayerConfigJson | string)[],

View file

@ -13,6 +13,7 @@ import * as bike_shops from "../assets/layers/bike_shop/bike_shop.json"
import * as bike_cleaning from "../assets/layers/bike_cleaning/bike_cleaning.json"
import * as maps from "../assets/layers/maps/maps.json"
import * as information_boards from "../assets/layers/information_board/information_board.json"
import * as direction from "../assets/layers/direction/direction.json"
import LayerConfig from "./JSON/LayerConfig";
export default class SharedLayers {
@ -37,6 +38,7 @@ export default class SharedLayers {
new LayerConfig(bike_shops, "shared_layers"),
new LayerConfig(bike_cleaning, "shared_layers"),
new LayerConfig(maps, "shared_layers"),
new LayerConfig(direction, "shared_layers"),
new LayerConfig(information_boards, "shared_layers")
];

View file

@ -35,6 +35,9 @@ import Svg from "./Svg";
import Link from "./UI/Base/Link";
import * as personal from "./assets/themes/personalLayout/personalLayout.json"
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import * as L from "leaflet";
import {Img} from "./UI/Img";
import {UserDetails} from "./Logic/Osm/OsmConnection";
export class InitUiElements {
@ -142,6 +145,7 @@ export class InitUiElements {
}
if (layoutToUse.id === personal.id) {
State.state.favouriteLayers.addCallback(updateFavs);
State.state.installedThemes.addCallback(updateFavs);
@ -153,6 +157,10 @@ export class InitUiElements {
* This is given to the div which renders fullscreen on mobile devices
*/
State.state.selectedElement.addCallback((feature) => {
if (feature === undefined) {
State.state.fullScreenMessage.setData(undefined);
}
if (feature?.properties === undefined) {
return;
}
@ -163,9 +171,15 @@ export class InitUiElements {
continue;
}
const applicable = layer.overpassTags.matches(TagUtils.proprtiesToKV(data));
if (applicable) {
// This layer is the layer that gives the questions
if (!applicable) {
continue;
}
if(layer.title === null && layer.tagRenderings.length === 0){
continue;
}
// This layer is the layer that gives the questions
const featureBox = new FeatureInfoBox(
State.state.allElements.getElement(data.id),
layer
@ -175,7 +189,6 @@ export class InitUiElements {
break;
}
}
}
);
InitUiElements.OnlyIf(State.state.featureSwitchUserbadge, () => {
@ -204,6 +217,21 @@ export class InitUiElements {
content.AttachTo("messagesbox");
}
State.state.osmConnection.userDetails.map((userDetails: UserDetails) => userDetails?.home)
.addCallbackAndRun(home => {
if (home === undefined) {
return;
}
const color = getComputedStyle(document.body).getPropertyValue("--subtle-detail-color")
const icon = L.icon({
iconUrl: Img.AsData(Svg.home_white_bg.replace(/#ffffff/g, color)),
iconSize: [30, 30],
iconAnchor: [15, 15]
});
const marker = L.marker([home.lat, home.lon], {icon: icon})
marker.addTo(State.state.bm.map)
console.log(marker)
});
new GeoLocationHandler()
.SetStyle(`position:relative;display:block;border: solid 2px #0005;cursor: pointer; z-index: 999; /*Just below leaflets zoom*/background-color: white;border-radius: 5px;width: 43px;height: 43px;`)
@ -327,6 +355,10 @@ export class InitUiElements {
checkbox.isEnabled.setData(false);
})
State.state.selectedElement.addCallback(() => {
checkbox.isEnabled.setData(false);
})
const fullOptions2 = this.CreateWelcomePane();
State.state.fullScreenMessage.setData(fullOptions2)
@ -435,13 +467,15 @@ export class InitUiElements {
return new Combine([mapComplete, reportBug, " | ", stats, " | ", editHere, editWithJosm]).Render();
}, [State.state.osmConnection.userDetails])
).SetClass("map-attribution")
}
static InitBaseMap() {
const bm = new Basemap("leafletDiv", State.state.locationControl, this.CreateAttribution());
State.state.bm = bm;
bm.map.on("popupclose", () => {
State.state.selectedElement.setData(undefined)
})
State.state.layerUpdater = new UpdateFromOverpass(State.state);
State.state.availableBackgroundLayers = new AvailableBaseLayers(State.state).availableEditorLayers;
@ -475,13 +509,12 @@ export class InitUiElements {
throw "Layer " + layer + " was not substituted";
}
const flayer: FilteredLayer = new FilteredLayer(layer,
(tagsES) => {
return new FeatureInfoBox(
tagsES,
layer,
)
});
let generateContents = (tags: UIEventSource<any>) => new FeatureInfoBox(tags, layer);
if (layer.title === undefined && (layer.tagRenderings ?? []).length === 0) {
generateContents = undefined;
}
const flayer: FilteredLayer = new FilteredLayer(layer, generateContents);
flayers.push(flayer);
QueryParameters.GetQueryParameter("layer-" + layer.id, "true", "Wehter or not layer " + layer.id + " is shown")

View file

@ -6,6 +6,7 @@ import {GeoOperations} from "./GeoOperations";
import {UIElement} from "../UI/UIElement";
import State from "../State";
import LayerConfig from "../Customizations/JSON/LayerConfig";
import Hash from "./Web/Hash";
/***
* A filtered layer is a layer which offers a 'set-data' function
@ -75,12 +76,14 @@ export class FilteredLayer {
const selfFeatures = [];
for (let feature of geojson.features) {
const tags = TagUtils.proprtiesToKV(feature.properties);
if (!this.filters.matches(tags)) {
leftoverFeatures.push(feature);
continue;
}
const matches = this.filters.matches(tags);
if (matches) {
selfFeatures.push(feature);
}
if (!matches || this.layerDef.passAllFeatures) {
leftoverFeatures.push(feature);
}
}
this.RenderLayer(selfFeatures)
@ -117,7 +120,6 @@ export class FilteredLayer {
// We fetch all the data we have to show:
let fusedFeatures = this.ApplyWayHandling(this.FuseData(features));
console.log("Fused:",fusedFeatures)
// And we copy some features as points - if needed
const data = {
@ -126,7 +128,6 @@ export class FilteredLayer {
}
let self = this;
console.log(data);
this._geolayer = L.geoJSON(data, {
style: feature =>
self.layerDef.GenerateLeafletStyle(feature.properties),
@ -147,19 +148,21 @@ export class FilteredLayer {
color: style.color
});
} else {
if (style.icon.iconSize === undefined) {
style.icon.iconSize = [50, 50]
}
marker = L.marker(latLng, {
icon: L.icon(style.icon)
icon: L.divIcon(style.icon)
});
}
return marker;
},
onEachFeature: function (feature, layer: Layer) {
layer.on("click", (e) => {
if (self._showOnPopup === undefined) {
// No popup contents defined -> don't do anything
return;
}
function openPopup(latlng: any) {
if (layer.getPopup() === undefined
&& (window.screen.availHeight > 600 || window.screen.availWidth > 600) // We DON'T trigger this code on small screens! No need to create a popup
) {
@ -168,8 +171,7 @@ export class FilteredLayer {
closeOnEscapeKey: true,
}, layer);
// @ts-ignore
popup.setLatLng(e.latlng)
popup.setLatLng(latlng)
layer.bindPopup(popup);
const eventSource = State.state.allElements.addOrGetElement(feature);
@ -181,17 +183,35 @@ export class FilteredLayer {
// popup.openOn(State.state.bm.map);
// ANd we perform the pending update
uiElement.Update();
// @ts-ignore
popup.Update = () => {
uiElement.Update();
}
} else {
// @ts-ignore
layer.getPopup().Update();
}
// We set the element as selected...
State.state.selectedElement.setData(feature);
}
layer.on("click", (e) => {
// @ts-ignore
openPopup(e.latlng);
// We mark the event as consumed
L.DomEvent.stop(e);
});
if (feature.properties.id.replace(/\//g, "_") === Hash.Get().data) {
const center = GeoOperations.centerpoint(feature).geometry.coordinates;
openPopup({lat: center[1], lng: center[0]})
}
}
)
;
});
if (this.combinedIsDisplayed.data) {
this._geolayer.addTo(State.state.bm.map);

View file

@ -4,7 +4,6 @@ import {UIElement} from "../../UI/UIElement";
import State from "../../State";
import {Utils} from "../../Utils";
import {Basemap} from "./Basemap";
import {FixedUiElement} from "../../UI/Base/FixedUiElement";
import Svg from "../../Svg";
import {Img} from "../../UI/Img";
@ -48,15 +47,18 @@ export class GeoLocationHandler extends UIElement {
map.on('accuratepositionfound', onAccuratePositionFound);
map.on('accuratepositionerror', onAccuratePositionError);
FixedUiElement
State.state.currentGPSLocation.addCallback((location) => {
const color = getComputedStyle(document.body).getPropertyValue("--catch-detail-color")
const icon = L.icon(
{
iconUrl: Img.AsData(Svg.crosshair_blue),
iconUrl: Img.AsData(Svg.crosshair.replace(/#000000/g, color)),
iconSize: [40, 40], // size of the icon
iconAnchor: [20, 20], // point of the icon which will correspond to marker's location
})
State.state.currentGPSLocation.addCallback((location) => {
const newMarker = L.marker(location.latlng, {icon: icon});
newMarker.addTo(map);

View file

@ -62,6 +62,11 @@ export class UpdateFromOverpass {
if (state.locationControl.data.zoom < layer.minzoom) {
continue;
}
if(layer.doNotDownload){
continue;
}
// Check if data for this layer has already been loaded
let previouslyLoaded = false;
for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) {

18
Logic/Web/Hash.ts Normal file
View file

@ -0,0 +1,18 @@
import {UIEventSource} from "../UIEventSource";
export default class Hash {
public static Get() : UIEventSource<string>{
const hash = new UIEventSource<string>(window.location.hash.substr(1));
hash.addCallback(h => {
h = h.replace(/\//g, "_");
return window.location.hash = "#" + h;
});
window.onhashchange = () => {
hash.setData(window.location.hash.substr(1))
}
return hash;
}
}

View file

@ -2,6 +2,7 @@
* Wraps the query parameters into UIEventSources
*/
import {UIEventSource} from "../UIEventSource";
import Hash from "./Hash";
export class QueryParameters {
@ -57,7 +58,7 @@ export class QueryParameters {
parts.push(encodeURIComponent(key) + "=" + encodeURIComponent(QueryParameters.knownSources[key].data))
}
history.replaceState(null, "", "?" + parts.join("&"));
history.replaceState(null, "", "?" + parts.join("&") + "#" + Hash.Get().data);
}

View file

@ -12,6 +12,7 @@ import {LocalStorageSource} from "./Logic/Web/LocalStorageSource";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {BaseLayer} from "./Logic/BaseLayer";
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import Hash from "./Logic/Web/Hash";
/**
* Contains the global state: a bunch of UI-event sources
@ -22,7 +23,7 @@ export default class State {
// The singleton of the global state
public static state: State;
public static vNumber = "0.1.3-rc2+g";
public static vNumber = "0.1.3-rc4";
// The user journey states thresholds when a new feature gets unlocked
public static userJourney = {
@ -209,6 +210,22 @@ export default class State {
);
const h = Hash.Get();
this.selectedElement.addCallback(selected => {
if (selected === undefined) {
h.setData("");
} else {
h.setData(selected.id)
}
}
)
h.addCallbackAndRun(hash => {
if(hash === undefined || hash === ""){
self.selectedElement.setData(undefined);
}
})
this.installedThemes = this.osmConnection.preferencesHandler.preferences.map<{ layout: LayoutConfig, definition: string }[]>(allPreferences => {
const installedThemes: { layout: LayoutConfig, definition: string }[] = [];
if (allPreferences === undefined) {

7
Svg.ts
View file

@ -79,6 +79,11 @@ export default class Svg {
public static direction_svg() { return new FixedUiElement(Svg.direction);}
public static direction_ui() { return new FixedUiElement(Svg.direction_img);}
public static direction_gradient = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" width=\"100\" height=\"100\" viewBox=\"0 0 100 100\" version=\"1.1\" id=\"svg8\"> <metadata id=\"metadata8\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> <dc:title></dc:title> </cc:Work> </rdf:RDF> </metadata> <defs id=\"defs6\"> <linearGradient id=\"linearGradient820\"> <stop id=\"stop816\" offset=\"0\" style=\"stop-color:#000000;stop-opacity:1;\" /> <stop id=\"stop818\" offset=\"1\" style=\"stop-color:#000000;stop-opacity:0\" /> </linearGradient> <radialGradient gradientUnits=\"userSpaceOnUse\" gradientTransform=\"matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)\" r=\"28.883806\" fy=\"53.828533\" fx=\"49.787739\" cy=\"53.828533\" cx=\"49.787739\" id=\"radialGradient828\" xlink:href=\"#linearGradient820\" /> </defs> <path style=\"fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1\" d=\"M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z\" id=\"path821\" /> </svg> "
public static direction_gradient_img = Img.AsImageElement(Svg.direction_gradient)
public static direction_gradient_svg() { return new FixedUiElement(Svg.direction_gradient);}
public static direction_gradient_ui() { return new FixedUiElement(Svg.direction_gradient_img);}
public static down = " <svg xmlns:dc=\"http://purl.org/dc/elements/1.1/\" xmlns:cc=\"http://creativecommons.org/ns#\" xmlns:rdf=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\" xmlns:svg=\"http://www.w3.org/2000/svg\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:sodipodi=\"http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd\" xmlns:inkscape=\"http://www.inkscape.org/namespaces/inkscape\" version=\"1.0\" width=\"700\" height=\"700\" id=\"svg6\" sodipodi:docname=\"down.svg\" inkscape:version=\"0.92.4 (5da689c313, 2019-01-14)\"> <metadata id=\"metadata12\"> <rdf:RDF> <cc:Work rdf:about=\"\"> <dc:format>image/svg+xml</dc:format> <dc:type rdf:resource=\"http://purl.org/dc/dcmitype/StillImage\" /> </cc:Work> </rdf:RDF> </metadata> <defs id=\"defs10\" /> <sodipodi:namedview pagecolor=\"#ffffff\" bordercolor=\"#666666\" borderopacity=\"1\" objecttolerance=\"10\" gridtolerance=\"10\" guidetolerance=\"10\" inkscape:pageopacity=\"0\" inkscape:pageshadow=\"2\" inkscape:window-width=\"1920\" inkscape:window-height=\"1001\" id=\"namedview8\" showgrid=\"false\" inkscape:zoom=\"0.33714286\" inkscape:cx=\"477.91309\" inkscape:cy=\"350\" inkscape:window-x=\"0\" inkscape:window-y=\"0\" inkscape:window-maximized=\"1\" inkscape:current-layer=\"svg6\" /> <g transform=\"rotate(-180,342.1439,335.17672)\" id=\"g4\"> <path d=\"m -20,670.71582 c 0,-1.85843 349.99229,-699.98853 350.57213,-699.28671 1.94549,2.35478 350.06752,699.46087 349.427,699.71927 -0.41837,0.16878 -79.29725,-33.69092 -175.2864,-75.24377 l -174.52574,-75.55065 -174.2421,75.53732 c -95.83317,41.54551 -174.625237,75.5373 -175.093498,75.5373 -0.46826,0 -0.851382,-0.32075 -0.851382,-0.71276 z\" style=\"fill:#00ff00;stroke:none\" id=\"path2\" inkscape:connector-curvature=\"0\" /> </g> </svg> "
public static down_img = Img.AsImageElement(Svg.down)
public static down_svg() { return new FixedUiElement(Svg.down);}
@ -214,4 +219,4 @@ export default class Svg {
public static wikipedia_svg() { return new FixedUiElement(Svg.wikipedia);}
public static wikipedia_ui() { return new FixedUiElement(Svg.wikipedia_img);}
}
public static All = {"add.svg": Svg.add,"addSmall.svg": Svg.addSmall,"ampersand.svg": Svg.ampersand,"arrow-left-smooth.svg": Svg.arrow_left_smooth,"arrow-right-smooth.svg": Svg.arrow_right_smooth,"bug.svg": Svg.bug,"camera-plus.svg": Svg.camera_plus,"checkmark.svg": Svg.checkmark,"close.svg": Svg.close,"compass.svg": Svg.compass,"crosshair-blue-center.svg": Svg.crosshair_blue_center,"crosshair-blue.svg": Svg.crosshair_blue,"crosshair.svg": Svg.crosshair,"delete_icon.svg": Svg.delete_icon,"direction.svg": Svg.direction,"direction_gradient.svg": Svg.direction_gradient,"down.svg": Svg.down,"envelope.svg": Svg.envelope,"floppy.svg": Svg.floppy,"gear.svg": Svg.gear,"help.svg": Svg.help,"home.svg": Svg.home,"home_white_bg.svg": Svg.home_white_bg,"josm_logo.svg": Svg.josm_logo,"layers.svg": Svg.layers,"layersAdd.svg": Svg.layersAdd,"logo.svg": Svg.logo,"logout.svg": Svg.logout,"mapillary.svg": Svg.mapillary,"no_checkmark.svg": Svg.no_checkmark,"or.svg": Svg.or,"osm-logo-us.svg": Svg.osm_logo_us,"osm-logo.svg": Svg.osm_logo,"pencil.svg": Svg.pencil,"pop-out.svg": Svg.pop_out,"reload.svg": Svg.reload,"search.svg": Svg.search,"share.svg": Svg.share,"star.svg": Svg.star,"statistics.svg": Svg.statistics,"up.svg": Svg.up,"wikimedia-commons-white.svg": Svg.wikimedia_commons_white,"wikipedia.svg": Svg.wikipedia};}

View file

@ -14,7 +14,6 @@ export class FullScreenMessageBox extends UIElement {
constructor(onClear: (() => void)) {
super(State.state.fullScreenMessage);
this.HideOnEmpty(true);
const self = this;
this.returnToTheMap =
new Combine([Translations.t.general.returnToTheMap.Clone().SetStyle("font-size:xx-large")])

View file

@ -2,6 +2,10 @@ import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import Svg from "../../Svg";
import * as L from "leaflet"
import * as X from "leaflet-providers"
import {Basemap} from "../../Logic/Leaflet/Basemap";
import State from "../../State";
/**
* Selects a direction in degrees
@ -34,8 +38,8 @@ export default class DirectionInput extends InputElement<string> {
}
InnerRender(): string {
console.log("Inner render direction")
return new Combine([
`<div id="direction-leaflet-div-${this.id}" style="width:100%;height: 100%;position: absolute;top:0;left:0;border-radius:100%;"></div>`,
Svg.direction_svg().SetStyle(
`position: absolute;top: 0;left: 0;width: 100%;height: 100%;rotate:${this.value.data}deg;`)
.SetClass("direction-svg"),
@ -47,7 +51,6 @@ export default class DirectionInput extends InputElement<string> {
}
protected InnerUpdate(htmlElement: HTMLElement) {
console.log("Inner update direction")
super.InnerUpdate(htmlElement);
const self = this;

View file

@ -16,7 +16,9 @@ interface TextFieldDef {
explanation: string,
isValid: ((s: string, country?: string) => boolean),
reformat?: ((s: string, country?: string) => string),
inputHelper?: (value: UIEventSource<string>) => InputElement<string>,
inputHelper?: (value: UIEventSource<string>, options?: {
location: [number, number]
}) => InputElement<string>,
}
export default class ValidatedTextField {
@ -26,7 +28,9 @@ export default class ValidatedTextField {
explanation: string,
isValid?: ((s: string, country?: string) => boolean),
reformat?: ((s: string, country?: string) => string),
inputHelper?: (value: UIEventSource<string>) => InputElement<string>): TextFieldDef {
inputHelper?: (value: UIEventSource<string>, options?:{
location: [number, number]
}) => InputElement<string>): TextFieldDef {
if (isValid === undefined) {
isValid = () => true;
@ -197,7 +201,8 @@ export default class ValidatedTextField {
textArea?: boolean,
textAreaRows?: number,
isValid?: ((s: string, country: string) => boolean),
country?: string
country?: string,
location?: [number /*lat*/, number /*lon*/]
}): InputElement<string> {
options = options ?? {};
options.placeholder = options.placeholder ?? type;
@ -230,7 +235,9 @@ export default class ValidatedTextField {
}
if (tp.inputHelper) {
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue()));
input = new CombinedInputElement(input, tp.inputHelper(input.GetValue(),{
location: options.location
}));
}
return input;
}

View file

@ -28,7 +28,8 @@ export class FeatureInfoBox extends UIElement {
this._layerConfig = layerConfig;
this._title = new TagRenderingAnswer(tags, layerConfig.title)
this._title = layerConfig.title === undefined ? undefined :
new TagRenderingAnswer(tags, layerConfig.title)
.SetClass("featureinfobox-title");
this._titleIcons = new Combine(
layerConfig.titleIcons.map(icon => new TagRenderingAnswer(tags, icon)))

View file

@ -15,6 +15,9 @@ export default class TagRenderingAnswer extends UIElement {
super(tags);
this._tags = tags;
this._configuration = configuration;
if(configuration === undefined){
throw "Trying to generate a tagRenderingAnswer without configuration..."
}
}
InnerRender(): string {

View file

@ -251,7 +251,8 @@ export default class TagRenderingQuestion extends UIElement {
const textField = ValidatedTextField.InputForType(this._configuration.freeform.type, {
isValid: (str) => (str.length <= 255),
country: this._tags.data._country
country: this._tags.data._country,
location: [this._tags.data._lat, this._tags.data._lon]
});
textField.GetValue().setData(this._tags.data[this._configuration.freeform.key]);

View file

@ -39,12 +39,16 @@ export class SubstitutedTranslation extends UIElement {
return []
}
const tags = this.tags.data;
txt = SubstitutedTranslation.SubstituteKeys(txt, tags);
return this.EvaluateSpecialComponents(txt);
}
public static SubstituteKeys(txt: string, tags: any) {
for (const key in tags) {
// Poor mans replace all
txt = txt.split("{" + key + "}").join(tags[key]);
}
return this.EvaluateSpecialComponents(txt);
return txt;
}
private EvaluateSpecialComponents(template: string): UIElement[] {

View file

@ -94,15 +94,6 @@ export class UserBadge extends UIElement {
dryrun = new FixedUiElement("TESTING").SetClass("alert");
}
if (user.home !== undefined) {
const icon = L.icon({
iconUrl: Img.AsData(Svg.home_white_bg),
iconSize: [30, 30],
iconAnchor: [15, 15]
});
L.marker([user.home.lat, user.home.lon], {icon: icon}).addTo(State.state.bm.map)
}
const settings =
new Link(Svg.gear_svg(),
`https://www.openstreetmap.org/user/${encodeURIComponent(user.name)}/account`,

View file

@ -3,6 +3,7 @@ import * as $ from "jquery"
export class Utils {
public static readonly assets_path = "./assets/svg/";
static EncodeXmlValue(str) {
return str.replace(/&/g, '&amp;')
@ -167,4 +168,6 @@ export class Utils {
console.log("Added custom layout ",location)
}
}

View file

@ -0,0 +1,32 @@
{
"id": "direction",
"name": {
"en": "Direction visualization"
},
"minzoom": 16,
"overpassTags": {
"or": ["camera:direction~*","direction~*"]
},
"doNotDownload": true,
"passAllFeatures": true,
"title": null,
"description": {
"en": "This layer visualizes directions"
},
"tagRenderings": [],
"icon": "./assets/svg/direction_gradient.svg",
"rotation": {
"render": "{camera:direction}",
"mappings": [
{
"if": "direction~*",
"then": "{direction}"
}
]
},
"iconSize": "200,200,center",
"color": "--catch-detail-color",
"stroke": "0",
"presets": [],
"wayHandling": 2
}

View file

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="100"
height="100"
viewBox="0 0 100 100"
version="1.1"
id="svg8">
<metadata
id="metadata8">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs6">
<linearGradient
id="linearGradient820">
<stop
id="innercolor"
offset="0"
style="stop-color:#000000;stop-opacity:1;" />
<stop
id="outercolor"
offset="1"
style="stop-color:#000000;stop-opacity:0" />
</linearGradient>
<radialGradient
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.5439431,-0.01852438,0.02075364,1.7297431,-28.198837,-42.329969)"
r="28.883806"
fy="53.828533"
fx="49.787739"
cy="53.828533"
cx="49.787739"
id="radialGradient828"
xlink:href="#linearGradient820" />
</defs>
<path
style="fill:url(#radialGradient828);fill-opacity:1;stroke:none;stroke-width:0;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 49.787737,49.857275 20.830626,9.2566092 C 35.979158,-2.144159 60.514289,-3.8195259 78.598237,9.0063685 Z"
id="path821" />
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -10,3 +10,17 @@ html {
--shadow-color: #0f0 !important;
}
#innercolor {
stop-color:#ff0000
}
.leaflet-div-icon svg {
width: calc(100% - 3px);
height: calc(100% + 3px);
}
/*
.leaflet-div-icon svg path {
fill: none !important;
stroke-width: 1px !important;
stroke: #0f0 !important;
}
*/

View file

@ -27,6 +27,7 @@
"customCss": "./assets/themes/surveillance_cameras/custom_theme.css",
"defaultBackgroundId": "Stadia.AlidadeSmoothDark",
"layers": [
"direction",
{
"id": "cameras",
"name": {
@ -56,6 +57,7 @@
"tagRenderings": [
"images",
{
"#": "Camera type: fixed; panning; dome",
"question": {
"en": "What kind of camera is this?",
"nl": "Wat voor soort camera is dit?"
@ -97,18 +99,32 @@
]
},
{
"#": "direction. We don't ask this for a dome on a pole or ceiling as it has a 360° view",
"question": {
"en": "In which geographical direction does this camera film?",
"nl": "Naar welke geografische richting filmt deze camera?"
},
"render": "Films to {camera:direction}",
"condition": "camera:type!=dome",
"condition": {
"not": {
"and": [
"camera:type=dome",
{
"or": [
"camera:mount=ceiling",
"camera:mount=pole"
]
}
]
}
},
"freeform": {
"key": "camera:direction",
"type": "direction"
}
},
{
"#": "Operator",
"freeform": {
"key": "operator"
},
@ -122,6 +138,7 @@
}
},
{
"#": "Surveillance type: public, outdoor, indoor",
"question": {
"en": "What kind of surveillance is this camera",
"nl": "Wat soort bewaking wordt hier uitgevoerd?"
@ -134,8 +151,8 @@
]
},
"then": {
"en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station...",
"nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw..."
"en": "A public area is surveilled, such as a street, a bridge, a square, a park, a train station, a public corridor or tunnel,...",
"nl": "Bewaking van de publieke ruilmte, dus een straat, een brug, een park, een plein, een stationsgebouw, een publiek toegankelijke gang of tunnel..."
}
},
{
@ -156,13 +173,67 @@
]
},
"then": {
"nl": "Een private binnenruimte wordt bewaakt, bv. een wiinkel, een parkeergarage, ...",
"nl": "Een private binnenruimte wordt bewaakt, bv. een winkel, een parkeergarage, ...",
"en": "A private indoor area is surveilled, e.g. a shop, a private underground parking, ..."
}
}
]
},
{
"#": "Indoor camera? This isn't clear for 'public'-cameras",
"question": {
"en": "Is the public space surveilled by this camera an indoor or outdoor space?",
"nl": "Bevindt de bewaakte publieke ruimte camera zich binnen of buiten?"
},
"condition": {
"and": [
"surveillance:type=public"
]
},
"mappings": [
{
"if": "indoor=yes",
"then": {
"en": "This camera is located indoors",
"nl": "Deze camera bevindt zich binnen"
}
},
{
"if": "indoor=no",
"then": {
"en": "This camera is located outdoors",
"nl": "Deze camera bevindt zich buiten"
}
},
{
"if": "indoor=",
"then": {
"en": "This camera is probably located outdoors",
"nl": "Deze camera bevindt zich waarschijnlijk buiten"
},
"hideInAnswer": true
}
]
},
{
"#": "Level",
"question": {
"en": "On which level is this camera located?",
"nl": "Op welke verdieping bevindt deze camera zich?"
},
"freeform": {
"key": "level",
"type": "nat"
},
"condition": {
"or": [
"indoor=yes",
"surveillance:type=ye"
]
}
},
{
"#": "Surveillance:zone",
"question": {
"en": "What exactly is surveilled here?",
"nl": "Wat wordt hier precies bewaakt?"
@ -244,6 +315,7 @@
]
},
{
"#": "camera:mount",
"question": {
"en": "How is this camera placed?",
"nl": "Hoe is deze camera geplaatst?"
@ -267,10 +339,10 @@
}
},
{
"if": "camera:mount=pole",
"if": "camera:mount=ceiling",
"then": {
"en": "This camera is placed one a pole",
"nl": "Deze camera staat op een paal"
"en": "This camera is placed on the ceiling",
"nl": "Deze camera hangt aan het plafond"
}
}
]
@ -293,7 +365,7 @@
"render": "50,50,center"
},
"color": {
"render": "#00f"
"render": "#f00"
},
"presets": [
{

View file

@ -45,10 +45,6 @@ Contains tweaks for small screens
}
.leaflet-popup {
transform: unset !important;
}
.leaflet-popup-content {
/* On mobile, the popups are shown as a full-screen element */
display: none;
visibility: hidden;

View file

@ -38,6 +38,11 @@
}
.question svg {
width: 100%;
height: 100%;
}
.question-text {
font-size: larger;
font-weight: bold;

View file

@ -255,7 +255,6 @@ a {
position: absolute;
z-index: 5000;
transition: all 500ms linear;
overflow-x: hidden;
pointer-events: none;
/* Shadow offset */
padding: 0.5em 10px 0 0.5em;
@ -410,6 +409,12 @@ a {
overflow-y: auto;
overflow-x: hidden;
}
.leaflet-div-icon {
background-color: unset !important;
border: unset !important;
}
/****** ShareScreen *****/
.literal-code {

View file

@ -1 +0,0 @@
npm install

View file

@ -16,10 +16,11 @@
"start": "parcel *.html UI/** Logic/** assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"test": "ts-node test/*",
"generate:editor-layer-index": "cd assets/ && wget https://osmlab.github.io/editor-layer-index/imagery.geojson --output-document=editor-layer-index.json",
"generate:images": "ts-node generateIncludedImages.ts",
"generate:layouts": "ts-node createLayouts.ts",
"generate:images": "ts-node scripts/generateIncludedImages.ts",
"generate:translations": "ts-node scripts/generateTranslations.ts",
"generate:layouts": "ts-node scripts/createLayouts.ts",
"optimize-images": "cd assets/generated/ && find -name '*.png' -exec optipng '{}' \\; && echo 'PNGs are optimized'",
"generate": "npm run generate:images && npm run generate:layouts && npm run generate:editor-layer-index",
"generate": "npm run generate:images && npm run generate:translations && npm run generate:layouts && npm run generate:editor-layer-index",
"build": "rm -rf dist/ npm run generate && parcel build --public-url ./ *.html assets/** assets/**/** assets/**/**/** vendor/* vendor/*/*",
"prepare-deploy": "npm run generate && npm run build && rm -rf .cache",
"deploy:staging": "npm run prepare-deploy && rm -rf /home/pietervdvn/git/pietervdvn.github.io/Staging/* && cp -r dist/* /home/pietervdvn/git/pietervdvn.github.io/Staging/ && cd /home/pietervdvn/git/pietervdvn.github.io/ && git add * && git commit -m 'New MapComplete Version' && git push && cd - && npm run clean",

View file

@ -1,16 +1,16 @@
import {Img} from "./UI/Img"
import {UIElement} from "./UI/UIElement";
import {Img} from "../UI/Img"
import {UIElement} from "../UI/UIElement";
Img.runningFromConsole = true;
// We HAVE to mark this while importing
UIElement.runningFromConsole = true;
import {AllKnownLayouts} from "./Customizations/AllKnownLayouts";
import {AllKnownLayouts} from "../Customizations/AllKnownLayouts";
import {existsSync, mkdirSync, readFileSync, writeFile, writeFileSync} from "fs";
import Locale from "./UI/i18n/Locale";
import Locale from "../UI/i18n/Locale";
import svg2img from 'promise-svg2img';
import Translations from "./UI/i18n/Translations";
import {Translation} from "./UI/i18n/Translation";
import LayoutConfig from "./Customizations/JSON/LayoutConfig";
import Translations from "../UI/i18n/Translations";
import {Translation} from "../UI/i18n/Translation";
import LayoutConfig from "../Customizations/JSON/LayoutConfig";
function enc(str: string): string {
@ -100,7 +100,7 @@ const alreadyWritten = []
function createIcon(iconPath: string, size: number, layout: LayoutConfig) {
let name = iconPath.split(".").slice(0, -1).join(".");
if (name.startsWith("./")) {
if (name.startsWith("../")) {
name = name.substr(2)
}
const newname = `${name}${size}.png`
@ -151,7 +151,7 @@ function createManifest(layout: LayoutConfig, relativePath: string) {
let path = layout.icon;
if (layout.icon.startsWith("<")) {
// THis is already the svg
path = "./assets/generated/" + layout.id + "_logo.svg"
path = "../assets/generated/" + layout.id + "_logo.svg"
writeFileSync(path, layout.icon)
}
@ -212,19 +212,19 @@ function createLandingPage(layout: LayoutConfig) {
}
const og = `
<meta property="og:image" content="${ogImage ?? './assets/svg/add.svg'}">
<meta property="og:image" content="${ogImage ?? '../assets/svg/add.svg'}">
<meta property="og:title" content="${ogTitle}">
<meta property="og:description" content="${ogDescr}">`
let icon = layout.icon;
if (icon.startsWith("<?xml") || icon.startsWith("<svg")) {
// This already is an svg
icon = `./assets/generated/${layout.id}_icon.svg`
icon = `../assets/generated/${layout.id}_icon.svg`
writeFileSync(icon, layout.icon);
}
let output = template
.replace(`./manifest.manifest`, `./${enc(layout.id)}.webmanifest`)
.replace(`../manifest.manifest`, `../${enc(layout.id)}.webmanifest`)
.replace("<!-- $$$OG-META -->", og)
.replace(/<title>.+?<\/title>/, `<title>${ogTitle}</title>`)
.replace("Loading MapComplete, hang on...", `Loading MapComplete theme <i>${ogTitle}</i>...`)
@ -251,7 +251,7 @@ let wikiPage = "{|class=\"wikitable sortable\"\n" +
"|-";
const generatedDir = "./assets/generated";
const generatedDir = "../assets/generated";
if (! existsSync(generatedDir)) {
mkdirSync(generatedDir)
}

View file

@ -1,5 +1,5 @@
import * as fs from "fs";
import {Utils} from "./Utils";
import {Utils} from "../Utils";
function genImages() {
@ -7,6 +7,7 @@ function genImages() {
const dir = fs.readdirSync("./assets/svg")
let module = "import {Img} from \"./UI/Img\";\nimport {FixedUiElement} from \"./UI/Base/FixedUiElement\";\n\nexport default class Svg {\n\n\n";
const allNames: string[] = [];
for (const path of dir) {
if (!path.endsWith(".svg")) {
@ -26,47 +27,11 @@ function genImages() {
module += ` public static ${name}_img = Img.AsImageElement(Svg.${name})\n`
module += ` public static ${name}_svg() { return new FixedUiElement(Svg.${name});}\n`
module += ` public static ${name}_ui() { return new FixedUiElement(Svg.${name}_img);}\n\n`
allNames.push(`"${path}": Svg.${name}`)
}
module += `public static All = {${allNames.join(",")}};`
module += "}\n";
fs.writeFileSync("Svg.ts", module);
console.log("Done")
}
function isTranslation(tr: any): boolean {
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false;
}
}
return true;
}
function transformTranslation(obj: any, depth = 1) {
if (isTranslation(obj)) {
return `new Translation( ${JSON.stringify(obj)} )`
}
let values = ""
for (const key in obj) {
values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n"
}
return `{${values}}`;
}
function genTranslations() {
const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8"))
const transformed = transformTranslation(translations);
let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`;
module += " public static t = " + transformed;
module += "}"
fs.writeFileSync("AllTranslationAssets.ts", module);
}
genTranslations()
genImages()

View file

@ -0,0 +1,40 @@
import * as fs from "fs";
import {Utils} from "../Utils";
function isTranslation(tr: any): boolean {
for (const key in tr) {
if (typeof tr[key] !== "string") {
return false;
}
}
return true;
}
function transformTranslation(obj: any, depth = 1) {
if (isTranslation(obj)) {
return `new Translation( ${JSON.stringify(obj)} )`
}
let values = ""
for (const key in obj) {
values += (Utils.Times((_) => " ", depth)) + key + ": " + transformTranslation(obj[key], depth + 1) + ",\n"
}
return `{${values}}`;
}
function genTranslations() {
const translations = JSON.parse(fs.readFileSync("./assets/translations.json", "utf-8"))
const transformed = transformTranslation(translations);
let module = `import {Translation} from "./UI/i18n/Translation"\n\nexport default class AllTranslationAssets {\n\n`;
module += " public static t = " + transformed;
module += "}"
fs.writeFileSync("AllTranslationAssets.ts", module);
}
genTranslations()

View file

@ -25,7 +25,10 @@
</style>
</head>
<body>
<div class="question">
<div id="maindiv">'maindiv' not attached</div>
</div>
<div id="extradiv">'extradiv' not attached</div>
<script src="./test.ts"></script>
</body>

View file

@ -6,7 +6,7 @@ import {UIEventSource} from "./Logic/UIEventSource";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
const d = new UIEventSource("90");
new Direction(d).AttachTo("maindiv")
new Direction(d, [51.21576,3.22001]).AttachTo("maindiv")
new VariableUiElement(d.map(d => "" + d + "°")).AttachTo("extradiv")
UIEventSource.Chronic(25, () => {