First steps for a decent custom theme generator

This commit is contained in:
Pieter Vander Vennet 2020-08-31 02:59:47 +02:00
parent a57b7d93fa
commit 2052976909
82 changed files with 1880 additions and 1311 deletions

View file

@ -4,41 +4,35 @@ import {Groen} from "./Layouts/Groen";
import Cyclofix from "./Layouts/Cyclofix";
import {StreetWidth} from "./Layouts/StreetWidth";
import {GRB} from "./Layouts/GRB";
import {ClimbingTrees} from "./Layouts/ClimbingTrees";
import {Smoothness} from "./Layouts/Smoothness";
import {MetaMap} from "./Layouts/MetaMap";
import {Natuurpunt} from "./Layouts/Natuurpunt";
import {GhostBikes} from "./Layouts/GhostBikes";
import {FromJSON} from "./JSON/FromJSON";
import * as bookcases from "../assets/themes/bookcases/Bookcases.json";
import * as aed from "../assets/themes/aed/aed.json";
import * as toilets from "../assets/themes/toilets/toilets.json";
import * as artworks from "../assets/themes/artwork/artwork.json";
import * as cyclestreets from "../assets/themes/cyclestreets/cyclestreets.json";
import * as ghostbikes from "../assets/themes/ghostbikes/ghostbikes.json"
import {PersonalLayout} from "../Logic/PersonalLayout";
export class AllKnownLayouts {
public static allLayers: Map<string, LayerDefinition> = undefined;
public static layoutsList: Layout[] = [
new PersonalLayout(),
new Natuurpunt(),
new GRB(),
new Cyclofix(),
new GhostBikes(),
FromJSON.LayoutFromJSON(bookcases),
// FromJSON.LayoutFromJSON(aed),
// FromJSON.LayoutFromJSON(toilets),
// FromJSON.LayoutFromJSON(artworks),
// FromJSON.LayoutFromJSON(cyclestreets),
FromJSON.LayoutFromJSON(aed),
FromJSON.LayoutFromJSON(toilets),
FromJSON.LayoutFromJSON(artworks),
FromJSON.LayoutFromJSON(cyclestreets),
FromJSON.LayoutFromJSON(ghostbikes),
new MetaMap(),
new StreetWidth(),
new ClimbingTrees(),
new Smoothness(),
new Groen(),
];
@ -47,11 +41,15 @@ export class AllKnownLayouts {
public static allSets: Map<string, Layout> = AllKnownLayouts.AllLayouts();
private static AllLayouts(): Map<string, Layout> {
this.allLayers = new Map<string, LayerDefinition>();
for (const layout of this.layoutsList) {
for (const layer of layout.layers) {
for (let i = 0; i < layout.layers.length; i++) {
let layer = layout.layers[i];
if (typeof (layer) === "string") {
layer = layout.layers[i] = FromJSON.sharedLayers.get(layer);
}
if (this.allLayers[layer.id] !== undefined) {
continue;
}

View file

@ -13,10 +13,31 @@ import Translations from "../../UI/i18n/Translations";
import Combine from "../../UI/Base/Combine";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {ImageCarouselConstructor} from "../../UI/Image/ImageCarousel";
import * as drinkingWater from "../../assets/layers/drinking_water/drinking_water.json";
import * as ghostbikes from "../../assets/layers/ghost_bike/ghost_bike.json"
import * as viewpoint from "../../assets/layers/viewpoint/viewpoint.json"
import {Utils} from "../../Utils";
export class FromJSON {
public static sharedLayers: Map<string, LayerDefinition> = FromJSON.getSharedLayers();
private static getSharedLayers() {
const sharedLayers = new Map<string, LayerDefinition>();
const sharedLayersList = [
FromJSON.Layer(drinkingWater),
FromJSON.Layer(ghostbikes),
FromJSON.Layer(viewpoint),
];
for (const layer of sharedLayersList) {
sharedLayers.set(layer.id, layer);
}
return sharedLayers;
}
public static FromBase64(layoutFromBase64: string): Layout {
return FromJSON.LayoutFromJSON(JSON.parse(atob(layoutFromBase64)));
@ -139,19 +160,27 @@ export class FromJSON {
{
k: FromJSON.Tag(mapping.if),
txt: FromJSON.Translation(mapping.then),
hideInAnswer: mapping.hideInAnswer
hideInAnswer: mapping.hideInAnswer
})
);
return new TagRenderingOptions({
let rendering = new TagRenderingOptions({
question: FromJSON.Translation(json.question),
freeform: freeform,
mappings: mappings
});
if (json.condition) {
console.log("Applying confition ", json.condition)
return rendering.OnlyShowIf(FromJSON.Tag(json.condition));
}
return rendering;
}
public static SimpleTag(json: string): Tag {
const tag = json.split("=");
const tag = Utils.SplitFirst(json, "=");
return new Tag(tag[0], tag[1]);
}
@ -159,35 +188,39 @@ export class FromJSON {
if (typeof (json) == "string") {
const tag = json as string;
if (tag.indexOf("!~") >= 0) {
const split = tag.split("!~");
if(split[1] == "*"){
const split = Utils.SplitFirst(tag, "!~");
if (split[1] === "*") {
split[1] = ".*"
}
console.log(split)
return new RegexTag(
new RegExp(split[0]),
new RegExp(split[1]),
split[0],
new RegExp("^" + split[1] + "$"),
true
);
}
if (tag.indexOf("!=") >= 0) {
const split = tag.split("!=");
const split = Utils.SplitFirst(tag, "!=");
if (split[1] === "*") {
split[1] = ".*"
}
return new RegexTag(
new RegExp(split[0]),
new RegExp(split[1]),
split[0],
new RegExp("^" + split[1] + "$"),
true
);
}
if (tag.indexOf("~") >= 0) {
const split = tag.split("~");
if(split[1] == "*"){
const split = Utils.SplitFirst(tag, "~");
if (split[1] === "*") {
split[1] = ".*"
}
return new RegexTag(
new RegExp("^"+split[0]+"$"),
new RegExp("^"+split[1]+"$")
split[0],
new RegExp("^" + split[1] + "$")
);
}
const split = tag.split("=");
const split = Utils.SplitFirst(tag, "=");
return new Tag(split[0], split[1])
}
if (json.and !== undefined) {
@ -208,11 +241,23 @@ export class FromJSON {
}
}
public static Layer(json: LayerConfigJson): LayerDefinition {
console.log("Parsing ",json.name);
public static Layer(json: LayerConfigJson | string): LayerDefinition {
if (typeof (json) === "string") {
const cached = FromJSON.sharedLayers.get(json);
if (cached) {
return cached;
}
throw "Layer not yet loaded..."
}
console.log("Parsing ", json.name);
const tr = FromJSON.Translation;
const overpassTags = FromJSON.Tag(json.overpassTags);
const icon = FromJSON.TagRenderingWithDefault(json.icon, "layericon", "./assets/bug.svg");
const iconSize = FromJSON.TagRenderingWithDefault(json.iconSize, "iconSize", "40,40,center");
const color = FromJSON.TagRenderingWithDefault(json.color, "layercolor", "#0000ff");
const width = FromJSON.TagRenderingWithDefault(json.width, "layerwidth", "10");
const renderTags = {"id": "node/-1"}
@ -225,11 +270,38 @@ export class FromJSON {
}) ?? [];
function style(tags) {
const iconSizeStr = iconSize.GetContent(tags).txt.split(",");
const iconwidth = Number(iconSizeStr[0]);
const iconheight = Number(iconSizeStr[1]);
const iconmode = iconSizeStr[2];
const iconAnchor = [iconwidth / 2, iconheight / 2] // x, y
// If iconAnchor is set to [0,0], then the top-left of the icon will be placed at the geographical location
if (iconmode.indexOf("left") >= 0) {
iconAnchor[0] = 0;
}
if (iconmode.indexOf("right") >= 0) {
iconAnchor[0] = iconwidth;
}
if (iconmode.indexOf("top") >= 0) {
iconAnchor[1] = 0;
}
if (iconmode.indexOf("bottom") >= 0) {
iconAnchor[1] = iconheight;
}
// the anchor is always set from the center of the point
// x, y with x going right and y going down if the values are bigger
const popupAnchor = [0, -iconAnchor[1]+3];
return {
color: color.GetContent(tags).txt,
weight: width.GetContent(tags).txt,
icon: {
iconUrl: icon.GetContent(tags).txt
iconUrl: icon.GetContent(tags).txt,
iconSize: [iconwidth, iconheight],
popupAnchor: popupAnchor,
iconAnchor: iconAnchor
},
}
}

View file

@ -46,6 +46,13 @@ export interface LayerConfigJson {
* Note that this also doubles as the icon for this layer (rendered with the overpass-tags) ánd the icon in the presets.
*/
icon?: string | TagRenderingConfigJson;
/**
* A string containing "width,height" or "width,height,anchorpoint" where anchorpoint is any of 'center', 'top', 'bottom', 'left', 'right', 'bottomleft','topright', ...
* Default is '40,40,center'
*/
iconSize?: string | TagRenderingConfigJson;
/**
* The color for way-elements
*/
@ -67,8 +74,8 @@ export interface LayerConfigJson {
* Presets for this layer
*/
presets?: {
tags: string[],
title: string | any,
tags: string[],
description?: string | any,
}[],

View file

@ -83,12 +83,12 @@ export interface LayoutConfigJson {
* In order to prevent them to do too much damage, all the overpass-tags of the layers are taken and combined as OR.
* These tag renderings will only show up if the object matches this filter.
*/
roamingRenderings?: TagRenderingConfigJson[],
roamingRenderings?: (TagRenderingConfigJson | string)[],
/**
* The layers to display
*/
layers: LayerConfigJson[],
layers: (LayerConfigJson | string)[],

View file

@ -1,14 +0,0 @@
/**
* Read a tagconfig and converts it into a TagsFilter value
*/
import {AndOrTagConfigJson} from "./TagConfigJson";
export default class TagConfig {
public static fromJson(json: any): TagConfig {
const config: AndOrTagConfigJson = json;
return config;
}
}

View file

@ -75,7 +75,10 @@ export class LayerDefinition {
color: string,
weight?: number,
icon: {
iconUrl: string, iconSize?: number[], popupAnchor?: number[], iconAnchor?: number[]
iconUrl: string,
iconSize?: number[],
popupAnchor?: number[],
iconAnchor?: number[]
},
};

View file

@ -24,7 +24,7 @@ export default class BikeOtherShops extends LayerDefinition {
this.name = this.to.name
this.icon = "./assets/bike/non_bike_repair_shop.svg"
this.overpassFilter = new And([
new RegexTag(/^shop$/, /^bicycle$/, true),
new RegexTag("shop", /^bicycle$/, true),
new RegexTag(/^service:bicycle:/, /.*/),
])
this.presets = []

View file

@ -1,43 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import Translations from "../../UI/i18n/Translations";
import FixedText from "../Questions/FixedText";
import {And, Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
export class ClimbingTree extends LayerDefinition {
constructor() {
super("climbingtree");
const t = Translations.t.climbingTrees.layer;
this.title = new FixedText(t.title);
const icon = "./assets/themes/nature/tree.svg";
this.icon = icon;
this.description = t.description;
this.style = (tags) => {
return {
color: "#00aa00",
icon: {
iconUrl: icon,
iconSize: [50, 50]
}
}
}
const tags = [new Tag("natural","tree"),new Tag("sport","climbing")];
this.overpassFilter = new And(tags);
this.presets = [
{
title: t.title,
description: t.description,
tags: tags
}
]
this.minzoom = 12;
this.elementsToShow = [
new ImageCarouselWithUploadConstructor()
]
}
}

View file

@ -1,62 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/Tags";
import {OperatorTag} from "../Questions/OperatorTag";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import Translations from "../../UI/i18n/Translations";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class DrinkingWater extends LayerDefinition {
constructor() {
super("drinkingwater");
this.name = Translations.t.cyclofix.drinking_water.title;
this.icon = "./assets/bike/drinking_water.svg";
this.overpassFilter = new Or([
new And([
new Tag("amenity", "drinking_water")
])
]);
this.presets = [{
title: Translations.t.cyclofix.drinking_water.title,
tags: [new Tag("amenity", "drinking_water")]
}];
this.maxAllowedOverlapPercentage = 10;
this.wayHandling = LayerDefinition.WAYHANDLING_CENTER_AND_WAY
this.minzoom = 13;
this.style = DrinkingWater.generateStyleFunction();
this.title = new FixedText("Drinking water");
this.elementsToShow = [
new OperatorTag(),
];
this.elementsToShow = [
new ImageCarouselWithUploadConstructor(),
new TagRenderingOptions({
question: "How easy is it to fill water bottles?",
mappings: [
{ k: new Tag("bottle", "yes"), txt: "It is easy to refill water bottles" },
{ k: new Tag("bottle", "no"), txt: "Water bottles may not fit" }
],
})];
}
private static generateStyleFunction() {
return function () {
return {
color: "#00bb00",
icon: {
iconUrl: "./assets/bike/drinking_water.svg",
iconSize: [50, 50],
iconAnchor: [25,50]
}
};
};
}
}

View file

@ -1,78 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import {Tag} from "../../Logic/Tags";
import FixedText from "../Questions/FixedText";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class GhostBike extends LayerDefinition {
constructor() {
super("ghost bike");
this.name = "Ghost bike";
this.overpassFilter = new Tag("memorial", "ghost_bike")
this.title = new FixedText("Ghost bike");
this.description = "A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident," +
" in the form of a white bicycle placed permanently near the accident location.";
this.minzoom = 1;
this.icon = "./assets/bike/ghost.svg"
this.presets = [
{
title: "Ghost bike",
description: "Add a missing ghost bike to the map",
tags: [new Tag("historic", "memorial"), new Tag("memorial", "ghost_bike")]
}
]
this.elementsToShow = [
new FixedText(this.description),
new ImageCarouselWithUploadConstructor(),
new TagRenderingOptions({
question: "Whom is remembered by this ghost bike?" +
"<span class='question-subtext'>" +
"<br/>" +
"Please respect privacy - only fill out the name if it is widely published or marked on the cycle." +
"</span>",
mappings: [{k: new Tag("noname", "yes"), txt: "There is no name marked on the bike"},],
freeform: {
key: "name",
extraTags: new Tag("noname", ""),
template: "$$$",
renderTemplate: "In the remembrance of <b>{name}</b>",
}
}),
new TagRenderingOptions({
question: "When was the ghost bike installed?",
freeform: {
key: "start_date",
template: "The ghost bike was placed on $$$", // TODO create a date picker
renderTemplate: "The ghost bike was placed on <b>{start_date}</b>",
}
}),
new TagRenderingOptions({
question: "On what URL can more information be found?" +
"<span class='question-subtext'>If available, add a link to a news report about the accident or about the placing of the ghost bike</span>",
freeform: {
key: "source",
template: "More information available on $$$",
renderTemplate: "<a href='{source}' target='_blank'>More information</a>",
}
}),
];
this.style = (tags: any) => {
return {
color: "#000000",
icon: {
iconUrl: 'assets/bike/ghost.svg',
iconSize: [40, 40],
iconAnchor: [20, 20],
}
}
};
}
}

View file

@ -1,49 +0,0 @@
import {LayerDefinition} from "../LayerDefinition";
import FixedText from "../Questions/FixedText";
import {Tag} from "../../Logic/Tags";
import {ImageCarouselWithUploadConstructor} from "../../UI/Image/ImageCarouselWithUpload";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class Viewpoint extends LayerDefinition {
constructor() {
super("viewpoint",{
name: "Bezienswaardigheid",
description: "Wil je een foto toevoegen van iets dat geen park, bos of natuurgebied is? Dit kan hiermee",
presets: [{
title: "Bezienswaardigheid (andere)",
description: "Wens je een foto toe te voegen dat geen park, bos of (erkend) natuurreservaat is? Dit kan hiermee",
tags: [new Tag("tourism", "viewpoint"),
new Tag("fixme", "Added with mapcomplete. This viewpoint should probably me merged with some existing feature")]
}],
icon: "assets/viewpoint.svg",
wayHandling: LayerDefinition.WAYHANDLING_CENTER_ONLY,
style: _ => {
return {
color: undefined, icon: {
iconUrl: "assets/viewpoint.svg",
iconSize: [20, 20]
}
}
},
maxAllowedOverlapPercentage: 0,
overpassFilter: new Tag("tourism", "viewpoint"),
minzoom: 13,
title: new FixedText("Bezienswaardigheid")
});
this.elementsToShow = [
new FixedText(this.description),
new ImageCarouselWithUploadConstructor(),
new TagRenderingOptions({
question: "Zijn er bijzonderheden die je wilt toevoegen?",
freeform:{
key: "description:0",
template: "$$$",
renderTemplate: "<h3>Bijzonderheden</h3>{description:0}"
}
})
]
}
}

View file

@ -1,6 +1,7 @@
import {LayerDefinition} from "../LayerDefinition";
import {And, Or, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
import {FromJSON} from "../JSON/FromJSON";
export class Widths extends LayerDefinition {
@ -39,6 +40,13 @@ export class Widths extends LayerDefinition {
[new Tag("highway", "pedestrian"), new Tag("highway", "living_street"),
new Tag("access","destination"), new Tag("motor_vehicle", "destination")])
private readonly _notCarfree =
FromJSON.Tag({"and":[
"highway!~pedestrian|living_street",
"access!~destination",
"motor_vehicle!~destination|no"
]});
private calcProps(properties) {
let parkingStateKnown = true;
let parallelParkingCount = 0;
@ -195,7 +203,7 @@ export class Widths extends LayerDefinition {
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._carfree, true),
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
@ -215,7 +223,7 @@ export class Widths extends LayerDefinition {
renderTemplate: "{note:width:carriageway}",
template: "$$$",
}
}).OnlyShowIf(this._carfree, true),
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({
@ -245,7 +253,7 @@ export class Widths extends LayerDefinition {
txt: "Tweerichtingsverkeer voor iedereen. Dit gebruikt <b>" + r(2 * this.carWidth + 2 * this.cyclistWidth) + "m</b>"
}
]
}).OnlyShowIf(this._carfree, true),
}).OnlyShowIf(this._notCarfree),
new TagRenderingOptions(
{
@ -263,7 +271,7 @@ export class Widths extends LayerDefinition {
{k: new Tag("short",""), txt: "De totale nodige ruimte voor vlot en veilig verkeer is dus <span class='thanks'>{targetWidth}m</span>"}
]
}
).OnlyShowIf(this._carfree, true),
).OnlyShowIf(this._notCarfree),
new TagRenderingOptions({

View file

@ -18,7 +18,7 @@ export class Layout {
public changesetMessage: string;
public socialImage: string = "";
public layers: LayerDefinition[];
public layers: (LayerDefinition | string)[];
public welcomeMessage: UIElement;
public gettingStartedPlzLogin: UIElement;
public welcomeBackMessage: UIElement;
@ -63,7 +63,7 @@ export class Layout {
id: string,
supportedLanguages: string[],
title: UIElement | string,
layers: LayerDefinition[],
layers: (LayerDefinition | string)[],
startzoom: number,
startLat: number,
startLon: number,

View file

@ -1,20 +0,0 @@
import Translations from "../../UI/i18n/Translations";
import {Layout} from "../Layout";
import {ClimbingTree} from "../Layers/ClimbingTree";
export class ClimbingTrees extends Layout {
constructor() {
super(
"climbing_trees",
["nl"],
Translations.t.climbingTrees.layout.title,
[new ClimbingTree()],
12,
50.8435,
4.3688,
Translations.t.climbingTrees.layout.welcome
);
this.icon = "./assets/themes/nature/tree.svg"
this.hideFromOverview = true;
}
}

View file

@ -3,7 +3,6 @@ import BikeParkings from "../Layers/BikeParkings";
import BikeServices from "../Layers/BikeStations";
import BikeShops from "../Layers/BikeShops";
import Translations from "../../UI/i18n/Translations";
import {DrinkingWater} from "../Layers/DrinkingWater";
import Combine from "../../UI/Base/Combine";
import BikeOtherShops from "../Layers/BikeOtherShops";
import BikeCafes from "../Layers/BikeCafes";
@ -15,7 +14,7 @@ export default class Cyclofix extends Layout {
"cyclofix",
["en", "nl", "fr","gl"],
Translations.t.cyclofix.title,
[new BikeServices(), new BikeShops(), new DrinkingWater(), new BikeParkings(), new BikeOtherShops(), new BikeCafes()],
[new BikeServices(), new BikeShops(), "drinking_water", new BikeParkings(), new BikeOtherShops(), new BikeCafes()],
16,
50.8465573,
4.3516970,

View file

@ -1,22 +0,0 @@
import {Layout} from "../Layout";
import {GhostBike} from "../Layers/GhostBike";
import Combine from "../../UI/Base/Combine";
export class GhostBikes extends Layout {
constructor() {
super("ghostbikes",
["en"],
"Ghost Bike Map",
[new GhostBike()],
6,
50.423,
5.493,
new Combine(["<h3>", "A map of Ghost Bikes", "</h3>",
"A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident," +
" in the form of a white bicycle placed permanently near the accident location.",
"On this map, one can see the location of known ghost bikes, and (with a free OpenStreetMap account) easily add missing and new Ghost Bikes"])
);
this.icon = "./assets/bike/ghost.svg";
}
}

View file

@ -2,7 +2,6 @@ import {NatureReserves} from "../Layers/NatureReserves";
import {Park} from "../Layers/Park";
import {Bos} from "../Layers/Bos";
import {Layout} from "../Layout";
import {Viewpoint} from "../Layers/Viewpoint";
export class Groen extends Layout {
@ -10,12 +9,12 @@ export class Groen extends Layout {
super("buurtnatuur",
["nl"],
"Buurtnatuur.be",
[new NatureReserves(), new Park(), new Bos(), new Viewpoint()],
[new NatureReserves(), new Park(), new Bos(), "viewpoint"],
10,
50.8435,
4.3688,
"\n" +
"<img src='assets/groen.svg' alt='logo-groen' class='logo'> <br />" +
"<img src='./assets/themes/buurtnatuur/groen_logo.svg' alt='logo-groen' class='logo'> <br />" +
"<h3>Breng jouw buurtnatuur in kaart</h3>" +
"<b>Natuur maakt gelukkig.</b> Aan de hand van deze website willen we de natuur dicht bij ons beter inventariseren. Met als doel meer mensen te laten genieten van toegankelijke natuur én te strijden voor meer natuur in onze buurten. \n" +
"<ul>" +
@ -52,9 +51,8 @@ export class Groen extends Layout {
"</small>"
);
this.icon = "./assets/groen.svg"
this.locationContains = ["buurtnatuur.be"]
this.socialImage = "assets/BuurtnatuurFront.jpg"
this.icon = "./assets/themes/buurtnatuur/groen_logo.svg"
this.socialImage = "assets/themes/buurtnatuur/social_image.jpg"
this.description = "Met deze tool kan je natuur in je buurt in kaart brengen en meer informatie geven over je favoriete plekje"
this.enableMoreQuests = false;
this.enableShareScreen = false

View file

@ -2,7 +2,6 @@ import {Layout} from "../Layout";
import {Birdhide} from "../Layers/Birdhide";
import {InformationBoard} from "../Layers/InformationBoard";
import {NatureReserves} from "../Layers/NatureReserves";
import {DrinkingWater} from "../Layers/DrinkingWater";
export class Natuurpunt extends Layout{
constructor() {
@ -10,7 +9,7 @@ export class Natuurpunt extends Layout{
"natuurpunt",
["nl"],
"De natuur in",
[new Birdhide(), new InformationBoard(), new NatureReserves(true), new DrinkingWater()],
[new Birdhide(), new InformationBoard(), new NatureReserves(true), "drinking_water"],
12,
51.20875,
3.22435,

View file

@ -1,82 +0,0 @@
import {Layout} from "../Layout";
import {LayerDefinition} from "../LayerDefinition";
import {Or, Tag} from "../../Logic/Tags";
import {TagRenderingOptions} from "../TagRenderingOptions";
export class SmoothnessLayer extends LayerDefinition {
constructor() {
super("smoothness");
this.name = "smoothness";
this.minzoom = 17;
this.overpassFilter = new Or([
new Tag("highway","unclassified"),
new Tag("highway", "residential"),
new Tag("highway", "cycleway"),
new Tag("highway", "footway"),
new Tag("highway", "path"),
new Tag("highway", "tertiary")
]);
this.elementsToShow = [
new TagRenderingOptions({
question: "How smooth is this road to rollerskate on",
mappings: [
{k: new Tag("smoothness","bad"), txt: "It's horrible"},
{k: new Tag("smoothness","intermediate"), txt: "It is passable by rollerscate, but only if you have to"},
{k: new Tag("smoothness","good"), txt: "Good, but it has some friction or holes"},
{k: new Tag("smoothness","very_good"), txt: "Quite good and enjoyable"},
{k: new Tag("smoothness","excellent"), txt: "Excellent - this is where you'd want to drive 24/7"},
]
})
]
this.style = (properties) => {
let color = "#000000";
if(new Tag("smoothness","bad").matchesProperties(properties)){
color = "#ff0000";
}
if(new Tag("smoothness","intermediate").matchesProperties(properties)){
color = "#ffaa00";
}
if(new Tag("smoothness","good").matchesProperties(properties)){
color = "#ccff00";
}
if(new Tag("smoothness","very_good").matchesProperties(properties)){
color = "#00aa00";
}
if(new Tag("smoothness","excellent").matchesProperties(properties)){
color = "#00ff00";
}
return {
color: color,
icon: undefined,
weight: 8
}
}
}
}
export class Smoothness extends Layout {
constructor() {
super(
"smoothness",
["en" ],
"Smoothness while rollerskating",
[new SmoothnessLayer()],
17,
51.2,
3.2,
"Give smoothness feedback for rollerskating"
);
this.widenFactor = 0.005
this.hideFromOverview = true;
this.enableAdd = false;
}
}

View file

@ -10,19 +10,16 @@ import Translation from "../UI/i18n/Translation";
export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
private readonly _tagsFilter: TagsFilter;
private readonly _embedded: TagDependantUIElementConstructor;
private readonly _invert: boolean;
constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor, invert: boolean = false) {
constructor(tagsFilter: TagsFilter, embedded: TagDependantUIElementConstructor) {
this._tagsFilter = tagsFilter;
this._embedded = embedded;
this._invert = invert;
}
construct(dependencies): TagDependantUIElement {
return new OnlyShowIf(dependencies.tags,
this._embedded.construct(dependencies),
this._tagsFilter,
this._invert);
this._tagsFilter);
}
IsKnown(properties: any): boolean {
@ -51,7 +48,7 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
}
private Matches(properties: any) : boolean{
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties)) != this._invert;
return this._tagsFilter.matches(TagUtils.proprtiesToKV(properties));
}
}
@ -59,22 +56,18 @@ export class OnlyShowIfConstructor implements TagDependantUIElementConstructor{
class OnlyShowIf extends UIElement implements TagDependantUIElement {
private readonly _embedded: TagDependantUIElement;
private readonly _filter: TagsFilter;
private readonly _invert: boolean;
constructor(
tags: UIEventSource<any>,
embedded: TagDependantUIElement,
filter: TagsFilter,
invert: boolean) {
filter: TagsFilter) {
super(tags);
this._filter = filter;
this._embedded = embedded;
this._invert = invert;
}
private Matches() : boolean{
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data)) != this._invert;
return this._filter.matches(TagUtils.proprtiesToKV(this._source.data));
}
InnerRender(): string {

View file

@ -0,0 +1,3 @@
export default class SharedLayers {
}

View file

@ -81,8 +81,8 @@ export class TagRenderingOptions implements TagDependantUIElementConstructor {
this.options = options;
}
OnlyShowIf(tagsFilter: TagsFilter, invert: boolean = false): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this, invert);
OnlyShowIf(tagsFilter: TagsFilter): TagDependantUIElementConstructor {
return new OnlyShowIfConstructor(tagsFilter, this);
}

View file

@ -21,6 +21,7 @@ import {UIEventSource} from "./Logic/UIEventSource";
import {QueryParameters} from "./Logic/Web/QueryParameters";
import {PersonalLayout} from "./Logic/PersonalLayout";
import {PersonalLayersPanel} from "./Logic/PersonalLayersPanel";
import Locale from "./UI/i18n/Locale";
export class InitUiElements {
@ -106,6 +107,14 @@ export class InitUiElements {
}
static CreateLanguagePicker(label: string | UIElement = "") {
return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
}
static InitLayerSelection() {
const closedFilterButton = `<button id="filter__button" class="filter__button shadow">${Img.closedFilterButton}</button>`;
@ -178,6 +187,10 @@ export class InitUiElements {
const state = State.state;
for (const layer of state.layoutToUse.data.layers) {
if(typeof (layer) === "string"){
throw "Layer "+layer+" was not substituted";
}
const generateInfo = (tagsES, feature) => {

View file

@ -223,12 +223,8 @@ export class FilteredLayer {
} else {
if(style.icon.iconSize === undefined){
style.icon.iconSize = [50,50]
}if(style.icon.iconAnchor === undefined){
style.icon.iconAnchor = [style.icon.iconSize[0] / 2,style.icon.iconSize[1]]
}
if (style.icon.popupAnchor === undefined) {
style.icon.popupAnchor = [0, 8 - (style.icon.iconSize[1])]
}
marker = L.marker(latLng, {
icon: new L.icon(style.icon),
});

View file

@ -53,12 +53,11 @@ export class LayerUpdater {
console.log("Not loading layer ", layer.id, " as it needs at least ", layer.minzoom, "zoom")
continue;
}
// Check if data for this layer has already been loaded
let previouslyLoaded = false;
for (let z = layer.minzoom; z < 25 && !previouslyLoaded; z++) {
const previousLoadedBounds = this.previousBounds.get(z);
if (previousLoadedBounds == undefined) {
if (previousLoadedBounds === undefined) {
continue;
}
for (const previousLoadedBound of previousLoadedBounds) {
@ -89,7 +88,7 @@ export class LayerUpdater {
self.runningQuery.setData(false);
if (geojson.features.length > 0) {
console.log("Got some leftovers: ", geojson)
console.warn("Got some leftovers: ", geojson)
}
return;
}

View file

@ -18,15 +18,14 @@ export class RegexTag extends TagsFilter {
private readonly value: RegExp;
private readonly invert: boolean;
constructor(key: RegExp, value: RegExp, invert: boolean = false) {
constructor(key: string | RegExp, value: RegExp, invert: boolean = false) {
super();
this.key = key;
this.key = typeof (key) === "string" ? new RegExp(key) : key;
this.value = value;
this.invert = invert;
}
asOverpass(): string[] {
return [`['${this.key.source}'${this.invert ? "!" : ""}~'${this.value.source}']`];
}
@ -45,7 +44,7 @@ export class RegexTag extends TagsFilter {
}
asHumanString() {
return `${this.key}${this.invert ? "!" : ""}~${this.value}`;
return `${this.key.source}${this.invert ? "!" : ""}~${this.value.source}`;
}
}
@ -64,7 +63,7 @@ export class Tag extends TagsFilter {
if(value === undefined){
throw "Invalid value";
}
if(value === undefined || value === "*"){
if(value === "*"){
console.warn(`Got suspicious tag ${key}=* ; did you mean ${key}!~*`)
}
}

View file

@ -8,17 +8,18 @@ export class UIEventSource<T>{
}
public addCallback(callback: ((latestData : T) => void)) {
public addCallback(callback: ((latestData : T) => void)) : UIEventSource<T>{
this._callbacks.push(callback);
return this;
}
public setData(t: T): void {
public setData(t: T): UIEventSource<T> {
if (this.data === t) {
return;
}
this.data = t;
this.ping();
return this;
}
public ping(): void {
@ -55,7 +56,6 @@ export class UIEventSource<T>{
const update = function () {
newSource.setData(f(self.data));
newSource.ping();
}
this.addCallback(update);

View file

@ -201,8 +201,15 @@ export class State {
continue;
}
try {
const layout = FromJSON.FromBase64(customLayout.data);
if(layout.id === undefined){
// This is an old style theme
// We remove it
customLayout.setData(undefined);
continue;
}
installedThemes.push({
layout: FromJSON.FromBase64(customLayout.data),
layout: layout,
definition: customLayout.data
});
} catch (e) {

View file

@ -17,6 +17,10 @@ export class SubtleButton extends UIElement{
}
InnerRender(): string {
if(this.message.IsEmpty()){
return "";
}
if(this.linkTo != undefined){
return new Combine([

View file

@ -15,13 +15,9 @@ export class TabbedComponent extends UIElement {
this.headers.push(Translations.W(element.header).onClick(() => self._source.setData(i)));
this.content.push(Translations.W(element.content));
}
}
InnerRender(): string {
let html = "";
let headerBar = "";
for (let i = 0; i < this.headers.length; i++) {
let header = this.headers[i];
@ -36,10 +32,11 @@ export class TabbedComponent extends UIElement {
headerBar = "<div class='tabs-header-bar'>" + headerBar + "</div>"
const content = this.content[this._source.data];
return headerBar + "<div class='tab-content'>" + content.Render() + "</div>";
return headerBar + "<div class='tab-content'>" + (content?.Render() ?? "") + "</div>";
}
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this.content[this._source.data].Update();
}

View file

@ -0,0 +1,73 @@
import {UIElement} from "../UIElement";
import {TabbedComponent} from "../Base/TabbedComponent";
import {SubtleButton} from "../Base/SubtleButton";
import {UIEventSource} from "../../Logic/UIEventSource";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import {LayerConfigJson} from "../../Customizations/JSON/LayerConfigJson";
import LayerPanel from "./LayerPanel";
import SingleSetting from "./SingleSetting";
export default class AllLayersPanel extends UIElement {
private panel: UIElement;
private _config: UIEventSource<LayoutConfigJson>;
private _currentlySelected: UIEventSource<SingleSetting<any>>;
private languages: UIEventSource<string[]>;
private static createEmptyLayer(): LayerConfigJson {
return {
id: undefined,
name: undefined,
minzoom: 0,
overpassTags: undefined,
title: undefined,
description: {}
}
}
constructor(config: UIEventSource<LayoutConfigJson>, currentlySelected: UIEventSource<SingleSetting<any>>,
languages: UIEventSource<any>) {
super(undefined);
this._config = config;
this._currentlySelected = currentlySelected;
this.languages = languages;
this.createPanels();
const self = this;
config.map<number>(config => config.layers.length).addCallback(() => self.createPanels());
}
private createPanels() {
const self = this;
const tabs = [];
const layers = this._config.data.layers;
for (let i = 0; i < layers.length; i++) {
tabs.push({
header: "<img src='./assets/bug.svg'>",
content: new LayerPanel(this._config, this.languages, i, this._currentlySelected)
});
}
tabs.push({
header: "<img src='./assets/add.svg'>",
content: new SubtleButton(
"./assets/add.svg",
"Add a new layer"
).onClick(() => {
self._config.data.layers.push(AllLayersPanel.createEmptyLayer())
self._config.ping();
})
})
this.panel = new TabbedComponent(tabs, new UIEventSource<number>(Math.max(0, layers.length-1)));
this.Update();
}
InnerRender(): string {
return this.panel.Render();
}
}

View file

@ -0,0 +1,84 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import Combine from "../Base/Combine";
import SettingsTable from "./SettingsTable";
import SingleSetting from "./SingleSetting";
import {TextField} from "../Input/TextField";
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
export default class GeneralSettingsPanel extends UIElement {
private panel: Combine;
public languages : UIEventSource<string[]>;
constructor(configuration: UIEventSource<LayoutConfigJson>, currentSetting: UIEventSource<SingleSetting<any>>) {
super(undefined);
const languagesField = new TextField<string[]>(
{
fromString: str => str?.split(";")?.map(str => str.trim().toLowerCase()),
toString: languages => languages.join(";"),
}
);
this.languages = languagesField.GetValue();
const version = TextField.StringInput();
const current_datetime = new Date();
let formatted_date = current_datetime.getFullYear() + "-" + (current_datetime.getMonth() + 1) + "-" + current_datetime.getDate() + " " + current_datetime.getHours() + ":" + current_datetime.getMinutes() + ":" + current_datetime.getSeconds()
version.GetValue().setData(formatted_date);
const locationRemark = "<br/>Note that, as soon as an URL-parameter sets the location or a location is known due to a previous visit, that the theme-set location is ignored"
const settingsTable = new SettingsTable(
[
new SingleSetting(configuration, TextField.StringInput(), "id",
"Identifier", "The identifier of this theme. This should be a lowercase, unique string"),
new SingleSetting(configuration, version, "version", "Version",
"A version to indicate the theme version. Ideal is the date you created or updated the theme"),
new SingleSetting(configuration, languagesField, "language",
"Supported languages", "Which languages do you want to support in this theme? Type the two letter code representing your language, seperated by <span class='literal-code'>;</span>. For example:<span class='literal-code'>en;nl</span> "),
new SingleSetting(configuration, new MultiLingualTextFields(this.languages), "title",
"Title", "The title as shown in the welcome message, in the browser title bar, in the more screen, ..."),
new SingleSetting(configuration, new MultiLingualTextFields(this.languages, true),
"description", "Description", "The description is shown in the welcomemessage. It is a small text welcoming users"),
new SingleSetting(configuration, TextField.StringInput(), "icon",
"Icon", "A visual representation for your theme; used as logo in the welcomeMessage. If your theme is official, used as favicon and webapp logo",
{
showIconPreview: true
}),
new SingleSetting(configuration, TextField.NumberInput("nat", n => n < 23), "startZoom","Initial zoom level",
"When a user first loads MapComplete, this zoomlevel is shown."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 90 && n > -90)), "startLat","Initial latitude",
"When a user first loads MapComplete, this latitude is shown as location."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("float", n => (n < 180 && n > -180)), "startLon","Initial longitude",
"When a user first loads MapComplete, this longitude is shown as location."+locationRemark),
new SingleSetting(configuration, TextField.NumberInput("pfloat", n => (n < 0.5 )), "widenFactor","Query widening",
"When a query is run, the data within bounds of the visible map is loaded.\n" +
"However, users tend to pan and zoom a lot. It is pretty annoying if every single pan means a reloading of the data.\n" +
"For this, the bounds are widened in order to make a small pan still within bounds of the loaded data.\n" +
"IF widenfactor is 0, this feature is disabled. A recommended value is between 0.5 and 0.01 (the latter for very dense queries)"),
new SingleSetting(configuration, TextField.StringInput(), "socialImage",
"og:image (aka Social Image)", "<span class='alert'>Only works on incorporated themes</span>" +
"The Social Image is set as og:image for the HTML-site and helps social networks to show a preview", {showIconPreview: true})
], currentSetting);
this.panel = new Combine([
"<h3>General theme settings</h3>",
settingsTable
]);
}
InnerRender(): string {
return this.panel.Render();
}
}

View file

@ -0,0 +1,87 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import SettingsTable from "./SettingsTable";
import SingleSetting from "./SingleSetting";
import {SubtleButton} from "../Base/SubtleButton";
import Combine from "../Base/Combine";
import {TextField} from "../Input/TextField";
import {InputElement} from "../Input/InputElement";
import MultiLingualTextFields from "../Input/MultiLingualTextFields";
import {CheckBox} from "../Input/CheckBox";
import {MultiTagInput} from "../Input/MultiTagInput";
/**
* Shows the configuration for a single layer
*/
export default class LayerPanel extends UIElement {
private _config: UIEventSource<LayoutConfigJson>;
private settingsTable: UIElement;
private deleteButton: UIElement;
constructor(config: UIEventSource<LayoutConfigJson>,
languages: UIEventSource<string[]>,
index: number,
currentlySelected: UIEventSource<SingleSetting<any>>) {
super(undefined);
this._config = config;
const actualDeleteButton = new SubtleButton(
"./assets/delete.svg",
"Yes, delete this layer"
).onClick(() => {
config.data.layers.splice(index, 1);
config.ping();
});
this.deleteButton = new CheckBox(
new Combine(
[
"<h3>Confirm layer deletion</h3>",
new SubtleButton(
"./assets/close.svg",
"No, don't delete"
),
"<span class='alert'>Deleting a layer can not be undone!</span>",
actualDeleteButton
]
),
new SubtleButton(
"./assets/delete.svg",
"Remove this layer"
)
)
function setting(input: InputElement<any>, path: string | string[], name: string, description: string | UIElement): SingleSetting<any> {
let pathPre = ["layers", index];
if (typeof (path) === "string") {
pathPre.push(path);
} else {
pathPre = pathPre.concat(path);
}
return new SingleSetting<any>(config, input, pathPre, name, description);
}
this.settingsTable = new SettingsTable([
setting(TextField.StringInput(), "id", "Id", "An identifier for this layer<br/>This should be a simple, lowercase, human readable string that is used to identify the layer."),
setting(new MultiLingualTextFields(languages), "title", "Title", "The human-readable name of this layer<br/>Used in the layer control panel and the 'Personal theme'"),
setting(new MultiLingualTextFields(languages, true), "description", "Description", "A description for this layer.<br/>Shown in the layer selections and in the personal theme"),
setting(new MultiTagInput(), "overpassTags","Overpass query",
new Combine(["The tags to load from overpass. ", MultiTagInput.tagExplanation]))
],
currentlySelected
)
;
}
InnerRender(): string {
return new Combine([
this.settingsTable,
this.deleteButton
]).Render();
}
}

View file

@ -0,0 +1,46 @@
import SingleSetting from "./SingleSetting";
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {InputElement} from "../Input/InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
export default class SettingsTable extends UIElement {
private _col1: UIElement[] = [];
private _col2: InputElement<any>[] = [];
public selectedSetting: UIEventSource<SingleSetting<any>>;
constructor(elements: SingleSetting<any>[],
currentSelectedSetting: UIEventSource<SingleSetting<any>>) {
super(undefined);
const self = this;
this.selectedSetting = currentSelectedSetting ?? new UIEventSource<SingleSetting<any>>(undefined);
for (const element of elements) {
let title: UIElement = new FixedUiElement(element._name);
this._col1.push(title);
this._col2.push(element._value);
element._value.IsSelected.addCallback(isSelected => {
if (isSelected) {
self.selectedSetting.setData(element);
} else if (self.selectedSetting.data === element) {
self.selectedSetting.setData(undefined);
}
})
}
}
InnerRender(): string {
let html = "";
for (let i = 0; i < this._col1.length; i++) {
html += `<tr><td>${this._col1[i].Render()}</td><td>${this._col2[i].Render()}</td></tr>`
}
return `<table><tr>${html}</tr></table>`;
}
}

View file

@ -0,0 +1,39 @@
import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
export default class SharePanel extends UIElement {
private _config: UIEventSource<LayoutConfigJson>;
private _panel: UIElement;
constructor(config: UIEventSource<LayoutConfigJson>, liveUrl: UIEventSource<string>) {
super(undefined);
this._config = config;
const json = new VariableUiElement(config.map(config => {
return JSON.stringify(config, null, 2)
.replace(/\n/g, "<br/>")
.replace(/ /g, "&nbsp;");
}));
this._panel = new Combine([
"<h2>share</h2>",
"Share the following link with friends:<br/>",
new VariableUiElement(liveUrl.map(url => `<a href='${url}' target="_blank">${url}</a>`)),
"<h3>Json</h3>",
"The json configuration is included for debugging purposes",
"<div class='literal-code json'>",
json,
"</div>"
]);
}
InnerRender(): string {
return this._panel.Render();
}
}

View file

@ -0,0 +1,84 @@
import {LayoutConfigJson} from "../../Customizations/JSON/LayoutConfigJson";
import {UIEventSource} from "../../Logic/UIEventSource";
import {InputElement} from "../Input/InputElement";
import {UIElement} from "../UIElement";
import Translations from "../i18n/Translations";
import Combine from "../Base/Combine";
import {VariableUiElement} from "../Base/VariableUIElement";
export default class SingleSetting<T> {
public _value: InputElement<T>;
public _name: string;
public _description: UIElement;
public _options: { showIconPreview?: boolean };
constructor(config: UIEventSource<LayoutConfigJson>,
value: InputElement<T>,
path: string | (string | number)[],
name: string,
description: string | UIElement,
options?: {
showIconPreview?: boolean
}
) {
this._value = value;
this._name = name;
this._description = Translations.W(description);
this._options = options ?? {};
if (this._options.showIconPreview) {
this._description = new Combine([
this._description,
"<h3>Icon preview</h3>",
new VariableUiElement(this._value.GetValue().map(url => `<img src='${url}' class="image-large-preview">`))
]);
}
if(typeof (path) === "string"){
path = [path];
}
const lastPart = path[path.length - 1];
path.splice(path.length - 1, 1);
function assignValue(value) {
if (value === undefined) {
return;
}
// We have to rewalk every time as parts might be new
let configPart = config.data;
for (const pathPart of path) {
configPart = configPart[pathPart];
if (configPart === undefined) {
console.warn("Lost the way for path ", path)
return;
}
}
configPart[lastPart] = value;
config.ping();
}
function loadValue() {
let configPart = config.data;
for (const pathPart of path) {
configPart = configPart[pathPart];
if (configPart === undefined) {
return;
}
}
const loadedValue = configPart[lastPart];
if (loadedValue !== undefined) {
value.GetValue().setData(loadedValue);
}
}
loadValue();
config.addCallback(() => loadValue());
value.GetValue().addCallback(assignValue);
assignValue(this._value.GetValue().data);
}
}

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,9 @@ export class DropDown<T> extends InputElement<T> {
private readonly _label: UIElement;
private readonly _values: { value: T; shown: UIElement }[];
private readonly _value : UIEventSource<T>;
private readonly _value: UIEventSource<T>;
public IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor(label: string | UIElement,
values: { value: T, shown: string | UIElement }[],
@ -17,8 +19,8 @@ export class DropDown<T> extends InputElement<T> {
this._value = value ?? new UIEventSource<T>(undefined);
this._label = Translations.W(label);
this._values = values.map(v => {
return {
value: v.value,
return {
value: v.value,
shown: Translations.W(v.shown)
}
}
@ -36,14 +38,6 @@ export class DropDown<T> extends InputElement<T> {
GetValue(): UIEventSource<T> {
return this._value;
}
ShowValue(t: T): boolean {
if (!this.IsValid(t)) {
return false;
}
this._value.setData(t);
}
IsValid(t: T): boolean {
for (const value of this._values) {
if (value.value === t) {

View file

@ -4,8 +4,9 @@ import {FixedUiElement} from "../Base/FixedUiElement";
import {UIEventSource} from "../../Logic/UIEventSource";
export class FixedInputElement<T> extends InputElement<T> {
private rendering: UIElement;
private value: UIEventSource<T>;
private readonly rendering: UIElement;
private readonly value: UIEventSource<T>;
public readonly IsSelected : UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor(rendering: UIElement | string, value: T) {
super(undefined);
@ -16,11 +17,6 @@ export class FixedInputElement<T> extends InputElement<T> {
GetValue(): UIEventSource<T> {
return this.value;
}
ShowValue(t: T): boolean {
return false;
}
InnerRender(): string {
return this.rendering.Render();
}
@ -28,7 +24,14 @@ export class FixedInputElement<T> extends InputElement<T> {
IsValid(t: T): boolean {
return t == this.value.data;
}
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
const self = this;
htmlElement.addEventListener("mouseenter", () => self.IsSelected.setData(true));
htmlElement.addEventListener("mouseout", () => self.IsSelected.setData(false))
}
}

View file

@ -1,10 +1,10 @@
import {UIElement} from "../UIElement";
import {FixedUiElement} from "../Base/FixedUiElement";
import {UIEventSource} from "../../Logic/UIEventSource";
export abstract class InputElement<T> extends UIElement{
abstract GetValue() : UIEventSource<T>;
abstract IsSelected: UIEventSource<boolean>;
abstract IsValid(t: T) : boolean;
}

View file

@ -0,0 +1,93 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {TextField} from "./TextField";
export default class MultiLingualTextFields extends InputElement<any> {
private _fields: Map<string, TextField<string>> = new Map<string, TextField<string>>();
private _value: UIEventSource<any>;
IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
constructor(languages: UIEventSource<string[]>,
textArea: boolean = false,
value: UIEventSource<Map<string, UIEventSource<string>>> = undefined) {
super(undefined);
this._value = value ?? new UIEventSource({});
const self = this;
function setup(languages: string[]) {
if(languages === undefined){
return;
}
const newFields = new Map<string, TextField<string>>();
for (const language of languages) {
if(language.length != 2){
continue;
}
let oldField = self._fields.get(language);
if (oldField === undefined) {
oldField = TextField.StringInput(textArea);
oldField.GetValue().addCallback(str => {
self._value.data[language] = str;
self._value.ping();
});
oldField.GetValue().setData(self._value.data[language]);
oldField.IsSelected.addCallback(() => {
let selected = false;
self._fields.forEach(value => {selected = selected || value.IsSelected.data});
self.IsSelected.setData(selected);
})
}
newFields.set(language, oldField);
}
self._fields = newFields;
self.Update();
}
setup(languages.data);
languages.addCallback(setup);
function load(latest: any){
if(latest === undefined){
return;
}
for (const lang in latest) {
self._fields.get(lang)?.GetValue().setData(latest[lang]);
}
}
this._value.addCallback(load);
load(this._value.data);
}
protected InnerUpdate(htmlElement: HTMLElement) {
super.InnerUpdate(htmlElement);
this._fields.forEach(value => value.Update());
}
GetValue(): UIEventSource<Map<string, UIEventSource<string>>> {
return this._value;
}
InnerRender(): string {
let html = "";
this._fields.forEach((field, lang) => {
html += `<tr><td>${lang}</td><td>${field.Render()}</td></tr>`
})
if(html === ""){
return "Please define one or more languages"
}
return `<table>${html}</table>`;
}
IsValid(t: any): boolean {
return true;
}
}

92
UI/Input/MultiTagInput.ts Normal file
View file

@ -0,0 +1,92 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {UIElement} from "../UIElement";
import Combine from "../Base/Combine";
import {SubtleButton} from "../Base/SubtleButton";
import TagInput from "./TagInput";
import {FixedUiElement} from "../Base/FixedUiElement";
export class MultiTagInput extends InputElement<string[]> {
public static tagExplanation: UIElement =
new FixedUiElement("<h3>How to use the tag-element</h3>")
private readonly _value: UIEventSource<string[]>;
IsSelected: UIEventSource<boolean>;
private elements: UIElement[] = [];
private inputELements: InputElement<string>[] = [];
private addTag: UIElement;
constructor(value: UIEventSource<string[]> = new UIEventSource<string[]>([])) {
super(undefined);
this._value = value;
this.addTag = new SubtleButton("./assets/addSmall.svg", "Add a tag")
.SetClass("small-button")
.onClick(() => {
this.IsSelected.setData(true);
value.data.push("");
value.ping();
});
const self = this;
value.map<number>((tags: string[]) => tags.length).addCallback(() => self.createElements());
this.createElements();
this._value.addCallback(tags => self.load(tags));
this.IsSelected = new UIEventSource<boolean>(false);
}
private load(tags: string[]) {
if (tags === undefined) {
return;
}
for (let i = 0; i < tags.length; i++) {
console.log("Setting tag ", i)
this.inputELements[i].GetValue().setData(tags[i]);
}
}
private UpdateIsSelected(){
this.IsSelected.setData(this.inputELements.map(input => input.IsSelected.data).reduce((a,b) => a && b))
}
private createElements() {
this.inputELements = [];
this.elements = [];
for (let i = 0; i < this._value.data.length; i++) {
let tag = this._value.data[i];
const input = new TagInput(new UIEventSource<string>(tag));
input.GetValue().addCallback(tag => {
console.log("Writing ", tag)
this._value.data[i] = tag;
this._value.ping();
}
);
this.inputELements.push(input);
input.IsSelected.addCallback(() => this.UpdateIsSelected());
const deleteBtn = new FixedUiElement("<img src='./assets/delete.svg' style='max-width: 1.5em; margin-left: 5px;'>")
.onClick(() => {
this._value.data.splice(i, 1);
this._value.ping();
});
this.elements.push(new Combine([input, deleteBtn, "<br/>"]).SetClass("tag-input-row"))
}
this.Update();
}
InnerRender(): string {
return new Combine([...this.elements, this.addTag]).SetClass("bordered").Render();
}
IsValid(t: string[]): boolean {
return false;
}
GetValue(): UIEventSource<string[]> {
return this._value;
}
}

107
UI/Input/TagInput.ts Normal file
View file

@ -0,0 +1,107 @@
import {InputElement} from "./InputElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import {DropDown} from "./DropDown";
import {TextField} from "./TextField";
import Combine from "../Base/Combine";
import {Utils} from "../../Utils";
export default class SingleTagInput extends InputElement<string> {
private readonly _value: UIEventSource<string>;
IsSelected: UIEventSource<boolean>;
private key: InputElement<string>;
private value: InputElement<string>;
private operator: DropDown<string>
constructor(value: UIEventSource<string> = undefined) {
super(undefined);
this._value = value ?? new UIEventSource<string>(undefined);
this.key = new TextField({
placeholder: "key",
fromString: str => {
if (str?.match(/^[a-zA-Z][a-zA-Z0-9:]*$/)) {
return str;
}
return undefined
},
toString: str => str
});
this.value = new TextField<string>({
placeholder: "value - if blank, matches if key is NOT present",
fromString: str => str,
toString: str => str
}
);
this.operator = new DropDown<string>("", [
{value: "=", shown: "="},
{value: "~", shown: "~"},
{value: "!~", shown: "!~"}
]);
this.operator.GetValue().setData("=");
const self = this;
function updateValue() {
if (self.key.GetValue().data === undefined ||
self.value.GetValue().data === undefined ||
self.operator.GetValue().data === undefined) {
return undefined;
}
self._value.setData(self.key.GetValue().data + self.operator.GetValue().data + self.value.GetValue().data);
}
this.key.GetValue().addCallback(() => updateValue());
this.operator.GetValue().addCallback(() => updateValue());
this.value.GetValue().addCallback(() => updateValue());
function loadValue(value: string) {
if (value === undefined) {
return;
}
let parts: string[];
if (value.indexOf("=") >= 0) {
parts = Utils.SplitFirst(value, "=");
self.operator.GetValue().setData("=");
} else if (value.indexOf("!~") > 0) {
parts = Utils.SplitFirst(value, "!~");
self.operator.GetValue().setData("!~");
} else if (value.indexOf("~") > 0) {
parts = Utils.SplitFirst(value, "~");
self.operator.GetValue().setData("~");
} else {
console.warn("Invalid value for tag: ", value)
return;
}
self.key.GetValue().setData(parts[0]);
self.value.GetValue().setData(parts[1]);
}
self._value.addCallback(loadValue);
loadValue(self._value.data);
this.IsSelected = this.key.IsSelected.map(
isSelected => isSelected || this.value.IsSelected.data, [this.value.IsSelected]
)
}
IsValid(t: string): boolean {
return false;
}
InnerRender(): string {
return new Combine([
this.key, this.operator, this.value
]).Render();
}
GetValue(): UIEventSource<string> {
return this._value;
}
}

View file

@ -14,7 +14,7 @@ export class ValidatedTextField {
"int": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str))},
"nat": (str) => {str = ""+str; return str !== undefined && str.indexOf(".") < 0 && !isNaN(Number(str)) && Number(str) > 0},
"float": (str) => !isNaN(Number(str)),
"pfloat": (str) => !isNaN(Number(str)) && Number(str) > 0,
"pfloat": (str) => !isNaN(Number(str)) && Number(str) >= 0,
"email": (str) => EmailValidator.validate(str),
"url": (str) => str,
"phone": (str, country) => {
@ -32,6 +32,33 @@ export class ValidatedTextField {
export class TextField<T> extends InputElement<T> {
public static StringInput(textArea: boolean = false): TextField<string> {
return new TextField<string>({
toString: str => str,
fromString: str => str,
textArea: textArea
});
}
public static NumberInput(type: string = "int", extraValidation: (number: Number) => boolean = undefined) : TextField<number>{
const isValid = ValidatedTextField.inputValidation[type];
extraValidation = extraValidation ?? (() => true)
return new TextField({
fromString: str => {
if(!isValid(str)){
return undefined;
}
const n = Number(str);
if(!extraValidation(n)){
return undefined;
}
return n;
},
toString: num => ""+num,
placeholder: type
});
}
private readonly value: UIEventSource<string>;
private readonly mappedValue: UIEventSource<T>;
@ -40,7 +67,8 @@ export class TextField<T> extends InputElement<T> {
private readonly _fromString?: (string: string) => T;
private readonly _toString: (t: T) => string;
private readonly startValidated: boolean;
public readonly IsSelected: UIEventSource<boolean> = new UIEventSource<boolean>(false);
private readonly _isArea: boolean;
constructor(options: {
/**
@ -61,11 +89,12 @@ export class TextField<T> extends InputElement<T> {
fromString: (string: string) => T,
value?: UIEventSource<T>,
startValidated?: boolean,
textArea?: boolean
}) {
super(undefined);
const self = this;
this.value = new UIEventSource<string>("");
this._isArea = options.textArea ?? false;
this.mappedValue = options?.value ?? new UIEventSource<T>(undefined);
this.mappedValue.addCallback(() => self.InnerUpdate());
@ -98,6 +127,11 @@ export class TextField<T> extends InputElement<T> {
return this.mappedValue;
}
InnerRender(): string {
if(this._isArea){
return `<textarea id="text-${this.id}" class="form-text-field" rows="4" cols="50" style="max-width: 100%;box-sizing: border-box"></textarea>`
}
return `<form onSubmit='return false' class='form-text-field'>` +
`<input type='text' placeholder='${this._placeholder.InnerRender()}' id='text-${this.id}'>` +
`</form>`;
@ -112,7 +146,7 @@ export class TextField<T> extends InputElement<T> {
this.mappedValue.addCallback((data) => {
field.className = data !== undefined ? "valid" : "invalid";
});
field.className = this.mappedValue.data !== undefined ? "valid" : "invalid";
const self = this;
@ -121,6 +155,9 @@ export class TextField<T> extends InputElement<T> {
self.value.setData(field.value);
};
field.addEventListener("focusin", () => self.IsSelected.setData(true));
field.addEventListener("focusout", () => self.IsSelected.setData(false));
field.addEventListener("keyup", function (event) {
if (event.key === "Enter") {
// @ts-ignore

View file

@ -23,6 +23,10 @@ export class MoreScreen extends UIElement {
if (layout === undefined) {
return undefined;
}
if(layout.id === undefined){
console.error("ID is undefined for layout",layout);
return undefined;
}
if (layout.hideFromOverview) {
if (State.state.osmConnection.GetPreference("hidden-theme-" + layout.id + "-enabled").data !== "true") {
return undefined;

View file

@ -17,7 +17,7 @@ export abstract class UIElement extends UIEventSource<string>{
*/
public static runningFromConsole = false;
protected constructor(source: UIEventSource<any>) {
protected constructor(source: UIEventSource<any> = undefined) {
super("");
this.id = "ui-element-" + UIElement.nextId;
this._source = source;
@ -146,6 +146,15 @@ export abstract class UIElement extends UIEventSource<string>{
if (this.clss.indexOf(clss) < 0) {
this.clss.push(clss);
}
this.Update();
return this;
}
public RemoveClass(clss: string): UIElement {
if (this.clss.indexOf(clss) >= 0) {
this.clss = this.clss.splice(this.clss.indexOf(clss), 1);
}
this.Update();
return this;
}

View file

@ -8,6 +8,7 @@ import {State} from "../State";
import {Utils} from "../Utils";
import {UIEventSource} from "../Logic/UIEventSource";
import {SubtleButton} from "./Base/SubtleButton";
import {InitUiElements} from "../InitUiElements";
/**
* Handles and updates the user badge
@ -23,7 +24,7 @@ export class UserBadge extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this._userDetails = State.state.osmConnection.userDetails;
this._languagePicker = Utils.CreateLanguagePicker();
this._languagePicker = InitUiElements.CreateLanguagePicker();
this._loginButton = Translations.t.general.loginWithOpenStreetMap
.Clone()
.SetClass("userbadge-login")

View file

@ -1,10 +1,10 @@
import {UIElement} from "../UI/UIElement";
import {UIElement} from "./UIElement";
import Locale from "../UI/i18n/Locale";
import {State} from "../State";
import {Layout} from "../Customizations/Layout";
import Translations from "./i18n/Translations";
import {Utils} from "../Utils";
import Combine from "./Base/Combine";
import {InitUiElements} from "../InitUiElements";
export class WelcomeMessage extends UIElement {
@ -19,7 +19,7 @@ export class WelcomeMessage extends UIElement {
constructor() {
super(State.state.osmConnection.userDetails);
this.ListenTo(Locale.language);
this.languagePicker = Utils.CreateLanguagePicker(Translations.t.general.pickLanguage);
this.languagePicker = InitUiElements.CreateLanguagePicker(Translations.t.general.pickLanguage);
function fromLayout(f: (layout: Layout) => (string | UIElement)): UIElement {
return Translations.W(f(State.state.layoutToUse.data));

View file

@ -760,14 +760,6 @@ export default class Translations {
fr: "{name} (vend des vélos)",
gl: "{name} (vende bicicletas)"
}),
},
drinking_water: {
title: new T({
en: 'Drinking water',
nl: "Drinkbaar water",
fr: "Eau potable",
gl: "Auga potábel"
})
}
},

View file

@ -1,7 +1,4 @@
import {UIElement} from "./UI/UIElement";
import {DropDown} from "./UI/Input/DropDown";
import {State} from "./State";
import Locale from "./UI/i18n/Locale";
export class Utils {
@ -25,7 +22,7 @@ export class Utils {
}
static DoEvery(millis: number, f: (() => void)) {
if (State.runningFromConsole) {
if (UIElement.runningFromConsole) {
return;
}
window.setTimeout(
@ -58,14 +55,6 @@ export class Utils {
return ls;
}
public static CreateLanguagePicker(label: string | UIElement = "") {
return new DropDown(label, State.state.layoutToUse.data.supportedLanguages.map(lang => {
return {value: lang, shown: lang}
}
), Locale.language);
}
public static EllipsesAfter(str : string, l : number = 100){
if(str.length <= l){
return str;
@ -96,5 +85,13 @@ export class Utils {
}
return t;
}
public static SplitFirst(a: string, sep: string):string[]{
const index = a.indexOf(sep);
if(index < 0){
return [a];
}
return [a.substr(0, index), a.substr(index+sep.length)];
}
}

288
assets/addSmall.svg Normal file
View file

@ -0,0 +1,288 @@
<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="98"
height="98"
viewBox="0 0 98 98"
version="1.1"
id="svg132"
sodipodi:docname="addSmall.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
style="fill:none">
<metadata
id="metadata136">
<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>
<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="namedview134"
showgrid="false"
showguides="true"
inkscape:guide-bbox="true"
inkscape:zoom="5.5166017"
inkscape:cx="9.5832222"
inkscape:cy="51.981151"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="svg132">
<sodipodi:guide
position="48.580633,-10.69499"
orientation="1,0"
id="guide959"
inkscape:locked="false" />
</sodipodi:namedview>
<circle
cx="48.999996"
cy="49.02142"
r="49"
id="circle4"
style="fill:#70c549" />
<defs
id="defs130">
<filter
id="filter0_d"
x="58.84"
y="52.703999"
width="25.4126"
height="17.436001"
filterUnits="userSpaceOnUse"
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood52" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix54" />
<feOffset
dy="4"
id="feOffset56" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur58" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
id="feColorMatrix60" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow"
id="feBlend62" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow"
result="shape"
id="feBlend64" />
</filter>
<filter
id="filter1_d"
x="14"
y="15"
width="38.000099"
height="38"
filterUnits="userSpaceOnUse"
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood67" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix69" />
<feOffset
dy="4"
id="feOffset71" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur73" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
id="feColorMatrix75" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow"
id="feBlend77" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow"
result="shape"
id="feBlend79" />
</filter>
<filter
id="filter2_d"
x="39.5"
y="7"
width="53"
height="53"
filterUnits="userSpaceOnUse"
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood82" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix84" />
<feOffset
dy="4"
id="feOffset86" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur88" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
id="feColorMatrix90" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow"
id="feBlend92" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow"
result="shape"
id="feBlend94" />
</filter>
<filter
id="filter3_d"
x="11"
y="54"
width="54.766701"
height="38.142899"
filterUnits="userSpaceOnUse"
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood97" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix99" />
<feOffset
dy="4"
id="feOffset101" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur103" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
id="feColorMatrix105" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow"
id="feBlend107" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow"
result="shape"
id="feBlend109" />
</filter>
<filter
id="filter4_d"
x="41"
y="64"
width="28"
height="29"
filterUnits="userSpaceOnUse"
style="color-interpolation-filters:sRGB">
<feFlood
flood-opacity="0"
result="BackgroundImageFix"
id="feFlood112" />
<feColorMatrix
in="SourceAlpha"
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"
id="feColorMatrix114" />
<feOffset
dy="4"
id="feOffset116" />
<feGaussianBlur
stdDeviation="2"
id="feGaussianBlur118" />
<feColorMatrix
type="matrix"
values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"
id="feColorMatrix120" />
<feBlend
mode="normal"
in2="BackgroundImageFix"
result="effect1_dropShadow"
id="feBlend122" />
<feBlend
mode="normal"
in="SourceGraphic"
in2="effect1_dropShadow"
result="shape"
id="feBlend124" />
</filter>
<clipPath
id="clip0">
<rect
width="31.819799"
height="31.819799"
transform="rotate(-45,57.35965,-37.759145)"
id="rect127"
x="0"
y="0"
style="fill:#ffffff" />
</clipPath>
</defs>
<g
transform="matrix(1.5647038,-1.5647038,1.5647038,1.5647038,-416.27812,-373.23804)"
id="layer1"
inkscape:label="Layer 1">
<path
inkscape:connector-curvature="0"
id="path815"
d="M 22.100902,291.35894 5.785709,275.04375 v 0"
style="fill:none;stroke:#ffffff;stroke-width:7.51411438;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
<path
inkscape:connector-curvature="0"
id="path815-3"
d="M 22.125504,274.96508 5.8103071,291.28027 v 0"
style="fill:none;stroke:#ffffff;stroke-width:7.51411438;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.9 KiB

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
sodipodi:docname="arrow-down.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-22.237738"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1680"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:3.59588718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 23.599389,276.87373 c 0,0 -7.53922,13.79953 -10.36091,13.84843 -2.82169,0.0489 -10.3860703,-13.84843 -10.3860703,-13.84843"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
sodipodi:docname="arrow-left-smooth.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="19.262262"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#ffffff;stroke-width:3.59588718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 20.139011,294.16029 c 0,0 -13.7995299,-7.53922 -13.8484369,-10.36091 -0.04891,-2.82169 13.8484369,-10.38607 13.8484369,-10.38607"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,82 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="arrow-right-both.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="19.262262"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:2.89327955;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 6.741713,274.0738 9.596044,9.69249 -9.5719331,9.66812"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
<path
style="fill:none;stroke:#000000;stroke-width:3.96875;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 31.419271,269.58152 c 0,0 13.197107,9.60301 13.243879,13.19711 0.04677,3.5941 -13.243879,13.22916 -13.243879,13.22916"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -1,81 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
sodipodi:docname="arrow-right-go-black.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="2.8284271"
inkscape:cx="46.174919"
inkscape:cy="90.659821"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="1050"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:3.69714379;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 13.091004,274.74719 c 0,0 8.270349,6.58048 8.299659,9.04335 0.02932,2.46286 -8.299659,9.0653 -8.299659,9.0653"
id="path821"
inkscape:connector-curvature="0" />
<path
style="fill:none;stroke:#000000;stroke-width:3.4395833;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="M 21.297864,283.77082 H 5.4219634 v 0"
id="path815"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.6 KiB

View file

@ -1,77 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
sodipodi:docname="arrow-right-sharp.svg">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="19.262262"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:2.89327955;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 6.741713,274.0738 9.596044,9.69249 -9.5719331,9.66812"
id="path819"
inkscape:connector-curvature="0"
sodipodi:nodetypes="ccc" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
sodipodi:docname="arrow-right-smooth.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-22.237738"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1920"
inkscape:window-height="1001"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#ffffff;stroke-width:3.59588718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 6.3128214,273.41335 c 0,0 13.7995296,7.53922 13.8484366,10.36091 0.04891,2.82169 -13.8484366,10.38607 -13.8484366,10.38607"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

View file

@ -1,76 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<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"
width="100"
height="100"
viewBox="0 0 26.458333 26.458334"
version="1.1"
id="svg8"
sodipodi:docname="arrow-up.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="4"
inkscape:cx="-22.237738"
inkscape:cy="36.323203"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
units="px"
showguides="true"
inkscape:guide-bbox="true"
inkscape:window-width="1680"
inkscape:window-height="1013"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1">
<sodipodi:guide
position="13.229167,23.859748"
orientation="1,0"
id="guide815"
inkscape:locked="false" />
<sodipodi:guide
position="14.944824,13.229167"
orientation="0,1"
id="guide817"
inkscape:locked="false" />
</sodipodi:namedview>
<metadata
id="metadata5">
<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>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(0,-270.54165)">
<path
style="fill:none;stroke:#000000;stroke-width:3.59588718;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 2.8524437,290.69991 c 0,0 7.5392203,-13.79953 10.3609103,-13.84843 2.82169,-0.0489 10.38607,13.84843 10.38607,13.84843"
id="path821"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.3 KiB

35
assets/floppy.svg Normal file
View file

@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="48" height="48">
<defs>
<linearGradient id="d">
<stop offset="0"/>
<stop offset="1" stop-opacity="0"/>
</linearGradient>
<linearGradient id="c">
<stop offset="0" stop-color="#858585"/>
<stop offset=".5" stop-color="#cbcbcb"/>
<stop offset="1" stop-color="#6b6b6b"/>
</linearGradient>
<linearGradient id="b">
<stop offset="0" stop-color="#fff"/>
<stop offset="1" stop-color="#fff" stop-opacity="0"/>
</linearGradient>
<linearGradient id="a">
<stop offset="0" stop-color="#1e2d69"/>
<stop offset="1" stop-color="#78a7e0"/>
</linearGradient>
<linearGradient id="f" x1="40.885" x2="16.88" y1="71.869" y2="-.389" gradientTransform="matrix(.97661 0 0 1.13979 .564 -3.271)" gradientUnits="userSpaceOnUse" xlink:href="#a"/>
<linearGradient id="g" x1="13.784" x2="33.075" y1="-.997" y2="55.702" gradientTransform="matrix(.98543 0 0 1.14818 .641 -2.934)" gradientUnits="userSpaceOnUse" xlink:href="#b"/>
<linearGradient id="h" x1="20.125" x2="28.563" y1="21.844" y2="42.469" gradientTransform="matrix(1.0677 0 0 1.12153 -1.369 -5.574)" gradientUnits="userSpaceOnUse" xlink:href="#c"/>
<radialGradient id="e" cx="24.313" cy="41.156" r="22.875" fx="24.313" fy="41.156" gradientTransform="matrix(1 0 0 .26913 0 30.08)" gradientUnits="userSpaceOnUse" xlink:href="#d"/>
</defs>
<path fill="url(#e)" d="M47.188 41.156a22.875 6.156 0 1 1-45.75 0 22.875 6.156 0 1 1 45.75 0z" opacity=".506" transform="matrix(.91803 0 0 .98122 1.68 .648)"/>
<path fill="url(#f)" stroke="#25375f" stroke-linecap="round" stroke-linejoin="round" d="M4.558 3.568h38.89c.59 0 1.064.474 1.064 1.063v37.765c0 .59-.475 1.064-1.064 1.064H6.558l-3.064-3.064V4.631a1.06 1.06 0 0 1 1.064-1.063z"/>
<path fill="#fff" d="M9 4h30v23H9z"/>
<rect width="30" height="4" x="9" y="4" fill="#d31c00" rx=".126" ry=".126"/>
<rect width="2" height="2" x="6" y="6" opacity=".739" rx=".126" ry=".126"/>
<path stroke="#000" d="M11 12.5h26m-26 5h26m-26 5h26" opacity=".131"/>
<path fill="none" stroke="url(#g)" stroke-linecap="round" d="M4.619 4.528h38.768c.07 0 .127.056.127.126v37.648c0 .07-.057.126-.127.126H6.928l-2.435-2.391V4.654c0-.07.056-.126.126-.126z" opacity=".597"/>
<path fill="url(#h)" stroke="#525252" d="M14.114 28.562h19.75c.888 0 1.603.751 1.603 1.684v13.201H12.51V30.246c0-.933.715-1.684 1.603-1.684z"/>
<rect width="5.03" height="10.066" x="16.464" y="30.457" fill="#4967a2" stroke="#525252" rx=".751" ry=".751"/>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

5
assets/layers.svg Normal file
View file

@ -0,0 +1,5 @@
<svg width="27" height="27" viewBox="0 0 27 27" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M26.5353 8.13481C26.4422 8.35428 26.2683 8.47598 26.0632 8.58537C21.9977 10.7452 17.935 12.9085 13.8758 15.0799C13.6475 15.2016 13.4831 15.1962 13.2568 15.0751C9.19822 12.903 5.13484 10.7404 1.07215 8.5758C0.490599 8.26608 0.448478 7.52562 0.991303 7.13796C1.0803 7.07438 1.17813 7.0231 1.2746 6.97045C5.15862 4.86462 9.04536 2.7629 12.9246 0.648187C13.3805 0.399316 13.7779 0.406837 14.2311 0.65434C18.0954 2.76153 21.9658 4.85779 25.8383 6.94926C26.1569 7.12155 26.411 7.32872 26.5353 7.67604C26.5353 7.82919 26.5353 7.98166 26.5353 8.13481Z" fill="#003B8B"/>
<path d="M13.318 26.535C12.1576 25.9046 10.9972 25.2736 9.83614 24.6439C6.96644 23.0877 4.09674 21.533 1.22704 19.9762C0.694401 19.6876 0.466129 19.2343 0.669943 18.7722C0.759621 18.5691 0.931505 18.3653 1.11969 18.2512C1.66659 17.9182 2.23727 17.6228 2.80863 17.3329C2.89423 17.2892 3.04981 17.3206 3.14493 17.3712C6.40799 19.1031 9.66969 20.837 12.9239 22.5845C13.3703 22.8238 13.7609 22.83 14.208 22.59C17.4554 20.8472 20.7117 19.1202 23.9605 17.3801C24.1493 17.2789 24.2838 17.283 24.4632 17.3876C24.8926 17.6386 25.3301 17.8772 25.7751 18.1001C26.11 18.2683 26.3838 18.4857 26.5346 18.8385C26.5346 18.9916 26.5346 19.1441 26.5346 19.2972C26.4049 19.6528 26.1399 19.8613 25.8152 20.0363C22.9964 21.5549 20.1831 23.0829 17.3684 24.609C16.1863 25.2496 15.0055 25.893 13.8248 26.535C13.6556 26.535 13.4865 26.535 13.318 26.535Z" fill="#003B8B"/>
<path d="M26.3988 13.7412C26.2956 13.9661 26.1026 14.081 25.8927 14.1924C21.8198 16.3577 17.749 18.5258 13.6815 20.7013C13.492 20.8025 13.3602 20.7902 13.1795 20.6938C9.09638 18.5114 5.01059 16.3359 0.924798 14.1582C0.399637 13.8786 0.307921 13.2646 0.735251 12.838C0.829005 12.7443 0.947217 12.6705 1.06407 12.6055C1.56545 12.3279 2.07635 12.0654 2.57297 11.7789C2.74214 11.6812 2.86579 11.6921 3.03291 11.7817C6.27492 13.5155 9.52303 15.2378 12.761 16.9792C13.2352 17.2343 13.6394 17.2322 14.1129 16.9772C17.3509 15.2358 20.5996 13.5142 23.8416 11.7796C24.0095 11.69 24.1338 11.6818 24.3016 11.7789C24.7384 12.0339 25.1821 12.2794 25.6352 12.5037C25.9701 12.6691 26.2426 12.8831 26.3995 13.2304C26.3988 13.4014 26.3988 13.5716 26.3988 13.7412Z" fill="#003B8B"/>
</svg>

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

@ -0,0 +1,58 @@
{
"id": "drinking_water",
"name": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel"
},
"title": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel"
},
"icon": "./assets/layers/drinking_water/drinking_water.svg",
"iconSize": "40,40,bottom",
"overpassTags": "amenity=drinking_water",
"minzoom": 13,
"wayHandling": 1,
"presets": [
{
"title": {
"en": "Drinking water",
"nl": "Drinkbaar water",
"fr": "Eau potable",
"gl": "Auga potábel"
},
"tags": ["amenity=drinking_water"]
}
],
"color": "#00bb00",
"tagRenderings": [
"images",
{
"question": {
"en": "How easy is it to fill water bottles?",
"nl": "Hoe gemakkelijk is het om drinkbussen bij te vullen?"
},
"mappings": [
{
"if": "bottle=yes",
"then": {
"en": "It is easy to refill water bottles",
"nl": "Een drinkbus bijvullen gaat makkelijk"
}
},
{
"if": "bottle=no",
"then": {
"en": "Water bottles may not fit",
"nl": "Een drinkbus past moeilijk"
}
}
]
}
]
}

View file

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View file

@ -0,0 +1,83 @@
{
"id": "ghost_bike",
"name": {
"en": "Ghost bikes",
"nl": "Witte Fietsen"
},
"overpassTags": "memorial=ghost_bike",
"minzoom": 0,
"title": {
"render": {
"en": "Ghost bike",
"nl": "Witte Fiets"
},
"mappings": [
{
"if": "name~*",
"then": {
"en": "Ghost bike in the remembrance of {name}",
"nl": "Witte fiets ter nagedachtenis van {name}"
}
}
]
},
"icon": "./assets/layers/ghost_bike/ghost_bike.svg",
"iconSize": "40,40,bottom",
"width": "5",
"color": "#000",
"wayHandling": 1,
"presets": [
{
"title": {
"en": "Ghost bike",
"nl": "Witte fiets"
},
"tags": [
"historic=memorial",
"memorial=ghost_bike"
]
}
],
"tagRenderings": [
{
"render": {
"en": "A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.",
"nl": "Een Witte Fiets (of Spookfiets) is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat over een witgeschilderde fiets die geplaatst werd in de buurt van het ongeval."
}
},
"images",
{
"question": {
"en": "Whom is remembered by this ghost bike?<span class='question-subtext'><br/>Please respect privacy - only fill out the name if it is widely published or marked on the cycle. Opt to leave out the family name.</span>",
"nl": "Aan wie is deze witte fiets een eerbetoon??<span class='question-subtext'><br/>Respecteer privacy - voeg enkel een naam toe indien die op de fiets staat of gepubliceerd is. Eventueel voeg je enkel de voornaam toe.</span>"
},
"render": {
"en": "In remembrance of {name}",
"nl": "Ter nagedachtenis van {name}"
},
"mappings": [
{
"if": "noname=yes",
"then": {
"en": "No name is marked on the bike",
"nl": "De naam is niet aangeduid op de fiets"
}
}
]
},
{
"question": {
"en": "On what webpage can one find more information about the Ghost bike or the accident?",
"nl": "Op welke website kan men meer informatie vinden over de Witte fiets of over het ongeval?"
},
"render": {
"en": "<a href='{source}' target='_blank'>More information is available</a>",
"nl": "<a href='{source}' target='_blank'>Meer informatie</a>"
},
"freeform": {
"type": "url",
"key": "source"
}
}
]
}

View file

@ -0,0 +1,44 @@
<svg width="98" height="122" viewBox="0 0 98 122" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M53.0445 112.094C51.2614 115.981 45.7386 115.981 43.9555 112.094L13.2124 45.085C11.6928 41.7729 14.1129 38 17.7569 38L79.2431 38C82.8871 38 85.3072 41.7729 83.7876 45.085L53.0445 112.094Z" fill="#171615"/>
<circle cx="49" cy="49" r="49" fill="#171615"/>
<g filter="url(#filter0_d)">
<path d="M56.4468 12.3002H49.8793V6.79007C49.8793 6.35374 49.5103 6 49.0552 6H47.2158C46.7606 6 46.3916 6.35374 46.3916 6.79007V12.3002H39.8242C39.369 12.3002 39 12.654 39 13.0903V14.8536C39 15.29 39.369 15.6437 39.8242 15.6437H46.3916V32.2099C46.3916 32.6463 46.7606 33 47.2158 33H49.0552C49.5103 33 49.8793 32.6463 49.8793 32.2099V15.6437H56.4468C56.902 15.6437 57.271 15.29 57.271 14.8536V13.0903C57.271 12.654 56.902 12.3002 56.4468 12.3002Z" fill="#FFFCFC"/>
</g>
<g filter="url(#filter1_d)">
<path d="M45.0763 63.8434H29.7114L38.0923 45.2455H46.4731H52.526H59.0444M59.0444 45.2455L56.7164 40H59.9756H63.2348M59.0444 45.2455L59.9756 47.6299L61.838 51.4448L66.9596 63.8434M59.0444 45.2455L54.6212 52.3985L51.9672 56.6903M50.1979 59.5516L51.9672 56.6903M41.3515 50.4911L46.0075 60.9822L36.6955 40L39.4891 45.7224M48.8011 61.9359L51.9672 56.6903" stroke="white" stroke-width="2"/>
<path d="M50.0922 63.032C50.0922 64.6108 48.8456 65.8701 47.3329 65.8701C45.8203 65.8701 44.5737 64.6108 44.5737 63.032C44.5737 61.4533 45.8203 60.194 47.3329 60.194C48.8456 60.194 50.0922 61.4533 50.0922 63.032Z" stroke="white"/>
<path d="M40.2801 62.0784C40.2801 68.1329 35.494 73 29.6401 73C23.7861 73 19 68.1329 19 62.0784C19 56.0238 23.7861 51.1567 29.6401 51.1567C35.494 51.1567 40.2801 56.0238 40.2801 62.0784Z" stroke="white" stroke-width="2"/>
</g>
<g filter="url(#filter2_d)">
<circle cx="66" cy="63" r="11" stroke="white" stroke-width="2"/>
</g>
<defs>
<filter id="filter0_d" x="35" y="6" width="26.271" height="35" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter1_d" x="14" y="39" width="64.12" height="43" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
<filter id="filter2_d" x="50" y="51" width="32" height="32" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
<feOffset dy="4"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.25 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,46 @@
{
"id": "viewpoint",
"name": {
"en": "Viewpoint",
"nl": "Uitzicht"
},
"description": {
"en": "A nice viewpoint or nice view. Ideal to add an image if no other category fits",
"nl": "Een mooi uitzicht - ideaal om een foto toe te voegen wanneer iets niet in een andere categorie past"
},
"overpassTags": "tourism=viewpoint",
"minzoom": 14,
"icon": "./assets/layers/viewpoint/viewpoint.svg",
"iconSize": "20,20,center",
"color": "#ffffff",
"width": "5",
"wayhandling": 2,
"presets": [
{
"title": {
"en": "Viewpoint",
"nl": "Uitzicht"
},
"tags": [
"tourism=viewpoint"
]
}
],
"title": {
"en": "Viewpoint",
"nl": "Uitzicht"
},
"tagRenderings": [
"images",
{
"question": {
"nl": "Zijn er bijzonderheden die je wilt toevoegen?",
"en": "Do you want to add a description?"
},
"render": "{description}",
"freeform": {
"key": "description"
}
}
]
}

View file

Before

Width:  |  Height:  |  Size: 5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve">
<metadata> Svg Vector Icons : http://www.onlinewebfonts.com/icon </metadata>
<g><path d="M500,10C229.4,10,10,229.4,10,500s219.4,490,490,490s490-219.4,490-490S770.6,10,500,10z M500,941C256.4,941,59,743.6,59,500S256.4,59,500,59s441,197.4,441,441S743.6,941,500,941z"/><path d="M296.4,831.8V215.3h196.5c73.9,0,113.9,3.8,136.3,9.8c34.5,9,64.4,30.6,87.6,60.8c23.3,30.2,33.9,72.4,33.9,120.2c0,36.9-6.7,74.7-20.1,99.9c-13.4,25.2-30.4,47.2-51.1,61.6c-20.7,14.4-41.7,23.9-63,28.5c-29,5.7-62.9,8.6-117.9,8.6h-87.1v227H296.4z M411.6,311.9v194.2H493c50.8,1.1,79.9-3.1,96-9.4c16.1-6.3,28.8-21.8,38-35.3c9.2-13.4,13.8-35.7,13.8-53.5c0-21.9-6.4-51-19.3-65.2c-12.9-14.2-29.1-23.1-48.8-26.7c-14.5-2.7-43.6-4.1-87.4-4.1L411.6,311.9L411.6,311.9L411.6,311.9z"/></g>
</svg>

Before

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -1,72 +0,0 @@
<?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:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 0 64 64"
height="64"
width="64"
id="svg109"
version="1.1"
sodipodi:docname="Statue.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="1680"
inkscape:window-height="1013"
id="namedview6"
showgrid="false"
inkscape:zoom="5.9599"
inkscape:cx="18.575009"
inkscape:cy="24.82515"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer2" />
<metadata
id="metadata115">
<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="defs113" />
<g
inkscape:groupmode="layer"
id="layer1"
inkscape:label="icon">
<g
inkscape:groupmode="layer"
id="layer2"
inkscape:label="background">
<circle
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.484;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
id="path824"
cx="32.215305"
cy="32.12027"
r="31.87973" />
</g>
<path
id="rect4517-0"
d="m 31.580949,1.8448297 c -2.450028,0 -4.438737,1.9887079 -4.438737,4.4387347 0,2.4500309 1.988708,4.4303956 4.438737,4.4303956 2.450028,0 4.438737,-1.9803647 4.438737,-4.4303956 0,-2.4500268 -1.988708,-4.4387347 -4.438737,-4.4387347 z M 27.142212,13.675897 c -1.559562,0 -2.953595,1.417115 -2.953595,2.953595 0,2.957448 5.172963,11.883099 5.172964,14.79301 v 8.86913 h 4.438736 v -8.86913 c 0,-2.85233 5.172964,-11.835562 5.172964,-14.79301 0,-1.636599 -1.323142,-2.953595 -2.953595,-2.953595 z m -8.376864,30.887602 v 4.271867 H 44.39655 v -4.271867 z m 4.271867,8.543734 v 8.543734 h 17.087468 v -8.543734 z"
style="opacity:1;fill:#734a08;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:9.03008842;stroke-linecap:square;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;paint-order:markers stroke fill"
inkscape:connector-curvature="0" />
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 KiB

View file

@ -1,2 +0,0 @@
{
}

View file

Before

Width:  |  Height:  |  Size: 4 KiB

After

Width:  |  Height:  |  Size: 4 KiB

View file

Before

Width:  |  Height:  |  Size: 957 KiB

After

Width:  |  Height:  |  Size: 957 KiB

View file

@ -11,19 +11,35 @@
"maintainer": "MapComlete",
"widenfactor": 0.05,
"roamingRenderings": [
"pictures",
{
"question": "Is deze straat een fietsstraat?",
"mappings": [
{
"then": "Deze straat is een fietsstraat",
"if": "cyclestreet=yes&proposed:cyclestreet!~*"
"if": {
"and": [
"cyclestreet=yes",
"proposed:cyclestreet="
]
},
"then": "Deze straat is een fietsstraat"
},
{
"then": "Deze straat wordt binnenkort een fietsstraat",
"if": "proposed:cyclestreet=yes&cyclestreet!~*"
"if": {
"and": [
"cyclestreet=",
"proposed:cyclestreet=yes"
]
},
"then": "Deze straat wordt binnenkort een fietsstraat"
},
{
"if": "cyclestreet!~*&proposed:cyclestreet!~*",
"if": {
"and": [
"cyclestreet=",
"proposed:cyclestreet="
]
},
"then": "Deze straat is geen fietsstraat"
}
]
@ -31,6 +47,7 @@
{
"question": "Wanneer wordt deze straat een fietsstraat?",
"render": "Deze straat wordt fietsstraat op {cyclestreet:start_date}",
"condition": "proposed:cyclestreet=yes",
"freeform": {
"type": "date",
"key": "cyclestreet:start_date"
@ -42,7 +59,12 @@
"id": "fietsstraat",
"name": "Fietsstraten",
"minzoom": 9,
"overpassTags": "cyclestreet=yes",
"overpassTags": {
"and": [
"cyclestreet=yes",
"traffic_sign="
]
},
"description": "Een fietsstraat is een straat waar gemotoriseerd verkeer een fietser niet mag inhalen.",
"title": "{name}",
"icon": "./assets/themes/cyclestreets/F111.svg",
@ -74,14 +96,14 @@
"name": "Alle straten",
"description": "Laag waar je een straat als fietsstraat kan markeren",
"overpassTags": "highway~residential|tertiary|unclassified",
"minzoom": "18",
"minzoom": 18,
"wayHandling": 0,
"title": {
"render": "Straat",
"mappings": [
{
"then": "{name}",
"if": "name~*"
"if": "name~*",
"then": "{name}"
}
]
},

View file

@ -0,0 +1,20 @@
{
"id": "ghostbikes",
"maintainer":"MapComplete",
"version": "2020-08-30",
"language": ["en","nl"],
"title": {
"en": "Ghost bikes",
"nl": "Witte Fietsen"
},
"description": {
"en": "A <b>ghost bike</b> is a memorial for a cyclist who died in a traffic accident, in the form of a white bicycle placed permanently near the accident location.<br/><br/>On this map, one can see all the ghost bikes which are known by OpenStreetMap. Is a ghost bike missing? Everyone can add or update information here - you only need to have a (free) OpenStreetMap account.",
"nl": "Een <b>Witte Fiets</b> of <b>Spookfiets</b> is een aandenken aan een fietser die bij een verkeersongeval om het leven kwam. Het gaat om een fiets die volledig wit is geschilderd en in de buurt van het ongeval werd geinstalleerd.<br/><br/>Op deze kaart zie je alle witte fietsen die door OpenStreetMap gekend zijn. Ontbreekt er een Witte Fiets of wens je informatie aan te passen? Meld je dan aan met een (gratis) OpenStreetMap account."
},
"icon": "./assets/layers/ghost_bike/ghost_bike.svg",
"startZoom": 1,
"startLat": 0,
"startLon": 0,
"widenFactor": 0.1,
"layers": ["ghost_bike"]
}

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 -256 1792 1792"><script xmlns="" id="__gaOptOutExtension"/>
<g transform="matrix(1,0,0,-1,53.152542,1217.0847)">
<path d="m 384,64 q 0,26 -19,45 -19,19 -45,19 -26,0 -45,-19 -19,-19 -19,-45 0,-26 19,-45 19,-19 45,-19 26,0 45,19 19,19 19,45 z m 644,420 -682,-682 q -37,-37 -90,-37 -52,0 -91,37 L 59,-90 Q 21,-54 21,0 21,53 59,91 L 740,772 Q 779,674 854.5,598.5 930,523 1028,484 z m 634,435 q 0,-39 -23,-106 Q 1592,679 1474.5,595.5 1357,512 1216,512 1031,512 899.5,643.5 768,775 768,960 q 0,185 131.5,316.5 131.5,131.5 316.5,131.5 58,0 121.5,-16.5 63.5,-16.5 107.5,-46.5 16,-11 16,-28 0,-17 -16,-28 L 1152,1120 V 896 l 193,-107 q 5,3 79,48.5 74,45.5 135.5,81 61.5,35.5 70.5,35.5 15,0 23.5,-10 8.5,-10 8.5,-25 z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 972 B

View file

@ -1,11 +1,11 @@
<!DOCTYPE html>
<html>
<html lang="en">
<head>
<link href="index.css" rel="stylesheet"/>
<title>Custom Theme Generator for Mapcomplete</title>
<style type="text/css">
#maindiv {
#left {
position: absolute;
width: 50vw;
height: 100vh;
@ -14,16 +14,39 @@
overflow-y: auto;
}
#preview {
#right {
position: absolute;
width: 50vw;
height: 100vh;
height: 35vh;
right: 0;
top: 0;
word-break: break-all;
overflow-y: auto;
}
#bottomright {
position: absolute;
width: 50vw;
height: 65vh;
right: 0;
bottom: 0;
overflow-y: auto;
}
.icon-preview {
max-width: 2em;
max-height: 2em ;
}
.image-large-preview {
max-width: 100%;
max-height: 30vh;
}
.json{
width:100%;
box-sizing: border-box;
}
.bordered {
border: 1px solid black;
display:block;
@ -31,6 +54,12 @@
border-radius: 0.5em;
}
.tag-input-row {
display: block ruby;
box-sizing: border-box;
margin-right: 2em;
}
body {
height: 100%;
}
@ -39,11 +68,11 @@
</head>
<body>
<div id="maindiv">
<h1>Create your own theme</h1>
<div id="layoutCreator"></div>
<div id="left">
'left' not attached
</div>
<div id="preview">'preview' not attached</div>
<div id="right">'right' not attached</div>
<div id="bottomright">'bottomright' not attached</div>
<script src="./customGenerator.ts"></script>
</body>
</html>

View file

@ -1,2 +1,112 @@
import {LayoutConfigJson} from "./Customizations/JSON/LayoutConfigJson";
import {UIEventSource} from "./Logic/UIEventSource";
import SingleSetting from "./UI/CustomGenerator/SingleSetting";
import Combine from "./UI/Base/Combine";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import GeneralSettings from "./UI/CustomGenerator/GeneralSettings";
import {SubtleButton} from "./UI/Base/SubtleButton";
import {TabbedComponent} from "./UI/Base/TabbedComponent";
import AllLayersPanel from "./UI/CustomGenerator/AllLayersPanel";
import SharePanel from "./UI/CustomGenerator/SharePanel";
const empty: LayoutConfigJson = {
id: "",
title: {},
description: {},
language: [],
maintainer: "",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
socialImage: "",
layers: [],
}
const test: LayoutConfigJson = {
id: "test",
title: {"en": "Test layout"},
description: {"en": "A layout for testing"},
language: ["en"],
maintainer: "Pieter Vander Vennet",
icon: "./assets/bug.svg",
version: "0",
startLat: 0,
startLon: 0,
startZoom: 1,
widenFactor: 0.05,
socialImage: "",
layers: [],
}
const es = new UIEventSource(test);
const encoded = es.map(config => btoa(JSON.stringify(config)));
const testUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}&test=true#${encoded}`)
const liveUrl = encoded.map(encoded => `./index.html?userlayout=${es.data.id}#${encoded}`)
const currentSetting = new UIEventSource<SingleSetting<any>>(undefined)
const generalSettings = new GeneralSettings(es, currentSetting);
const languages = generalSettings.languages;
new TabbedComponent([
{
header: "<img src='./assets/gear.svg'>",
content: generalSettings
},
{
header: "<img src='./assets/layers.svg'>",
content: new AllLayersPanel(es, currentSetting, languages)
},
{
header: "<img src='./assets/floppy.svg'>",
content: "Save"
},
{
header: "<img src='./assets/share.svg'>",
content: new SharePanel(es, liveUrl)
}
]).AttachTo("left");
const returnButton = new SubtleButton("./assets/close.svg",
new VariableUiElement(
currentSetting.map(currentSetting => {
if (currentSetting === undefined) {
return "";
}
return "Return to general help";
}
)
))
.ListenTo(currentSetting)
.onClick(() => currentSetting.setData(undefined));
const helpText = new VariableUiElement(currentSetting.map((setting: SingleSetting<any>) => {
if (setting === undefined) {
return "<h1>Welcome to the Custom Theme Builder</h1>" +
"Here, one can make their own custom mapcomplete themes.<br/>" +
"Fill out the fields to get a working mapcomplete theme. More information on the selected field will appear here when you click it";
}
return new Combine(["<h1>", setting._name, "</h1>", setting._description.Render()]).Render();
}))
new Combine([helpText,
returnButton,
]).AttachTo("right");
// The preview
new Combine([
new VariableUiElement(testUrl.map(testUrl => `<iframe src='${testUrl}' width='100%' height='99%' style="box-sizing: border-box" title='Theme Preview'></iframe>`))
]).AttachTo("bottomright");
console.log("TODO")

230
index.css
View file

@ -1,78 +1,78 @@
html, body {
height: 100%;
margin: 0;
padding: 0;
}
height: 100%;
margin: 0;
padding: 0;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
}
body {
font-family: 'Helvetica Neue', Arial, sans-serif;
}
form {
display: inline;
}
form {
display: inline;
}
.invalid {
box-shadow: 0 0 10px #ff5353;
}
.invalid {
box-shadow: 0 0 10px #ff5353;
}
.shadow {
box-shadow: 0 0 10px #00000066;
}
.shadow {
box-shadow: 0 0 10px #00000066;
}
#leafletDiv {
height: 100%;
}
#leafletDiv {
height: 100%;
}
#geolocate-button {
position: absolute;
bottom: 25px;
right: 50px;
z-index: 999; /*Just below leaflets zoom*/
background-color: white;
border-radius: 5px;
border: solid 2px #0005;
cursor: pointer;
width: 43px;
height: 43px;
display: none; /*Hidden by default, only visible on mobile*/
}
#geolocate-button {
position: absolute;
bottom: 25px;
right: 50px;
z-index: 999; /*Just below leaflets zoom*/
background-color: white;
border-radius: 5px;
border: solid 2px #0005;
cursor: pointer;
width: 43px;
height: 43px;
display: none; /*Hidden by default, only visible on mobile*/
}
#help-button-mobile {
display: none;
}
#help-button-mobile {
display: none;
}
#geolocate-button img{
width: 31px;
height:31px;
margin: 6px;
}
#geolocate-button img {
width: 31px;
height: 31px;
margin: 6px;
}
#geolocate-button > .uielement {
display: block;
}
#geolocate-button > .uielement {
display: block;
}
.selected-element {
fill: black
}
.selected-element {
fill: black
}
/**************** GENERIC ****************/
/**************** GENERIC ****************/
.uielement {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.uielement {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
}
.alert {
background-color: #fee4d1;
font-weight: bold;
border-radius: 1em;
padding: 0.3em;
margin: 0.25em;
.alert {
background-color: #fee4d1;
font-weight: bold;
border-radius: 1em;
padding: 0.3em;
margin: 0.25em;
text-align: center;
padding-top: 0.15em;
padding-bottom: 0.15em;
@ -1298,62 +1298,78 @@ form {
align-items: center;
text-decoration: none;
color: black;
}
.subtle-button a {
text-decoration: unset !important;
color: unset !important;
display: block ruby;
}
.subtle-button a {
text-decoration: unset !important;
color:unset !important;
display: block ruby;
}
.round-button .subtle-button {
width: 2em;
height: 2em;
border-radius: 1em;
display: block !important;
margin: 0;
padding: 0.5em;
}
.small-button .subtle-button {
height: 2em;
}
.small-button .subtle-button img {
max-height: 1.8em;
}
.subtle-button img {
max-width: 3em;
max-height: 3em;
margin-right: 0.5em;
padding: 0.5em;
}
.subtle-button img{
max-width: 3em;
max-height: 3em;
margin-right: 0.5em;
padding: 0.5em;
}
.add-ui {
font-size: large;
}
.add-popup-all-buttons {
max-height: 50vh;
display: inline-block;
overflow-y: auto;
width: 100%;
}
.custom-layer-panel {
}
.add-ui {
font-size: large;
}
.custom-layer-panel-header {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
font-size: large;
margin: 0.5em;
background-color: white;
justify-content: flex-start;
align-items: center;
text-decoration: none;
color: black;
}
.add-popup-all-buttons{
max-height: 50vh;
display: inline-block;
overflow-y: auto;
width: 100%;
}
.custom-layer-panel-header-img img {
max-width: 3em;
max-height: 3em;
padding: 0.5em;
}
.custom-layer-panel {
}
.custom-layer-panel-header {
display: flex;
flex-wrap: nowrap;
flex-direction: row;
font-size: large;
margin: 0.5em;
background-color: white;
justify-content: flex-start;
align-items: center;
text-decoration: none;
color: black;
}
.custom-layer-panel-header-img img {
max-width: 3em;
max-height: 3em;
padding: 0.5em;
}
.custom-layer-panel-header-img {
min-width: 4em;
height: 4em;
.custom-layer-panel-header-img {
min-width: 4em;
height: 4em;
}

View file

@ -3,7 +3,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>MapComplete</title>
<link rel="stylesheet" href="./vendor/leaflet.css"/>
<link rel="stylesheet" href="./index.css"/>

View file

@ -0,0 +1,9 @@
import TagInput from "./UI/Input/TagInput";
import {UIEventSource} from "./Logic/UIEventSource";
import {VariableUiElement} from "./UI/Base/VariableUIElement";
import {MultiTagInput} from "./UI/Input/MultiTagInput";
const input = new MultiTagInput(new UIEventSource<string[]>(["key~value|0"]));
input.GetValue().addCallback(console.log);
input.AttachTo("maindiv");
new VariableUiElement(input.GetValue().map(tags => tags.join(" & "))).AttachTo("extradiv")

View file

@ -46,14 +46,13 @@ new T([
freeform: {
key: "name",
},
fixedInputField: {
mappings: [
{
if: "noname=yes",
"then": "Has no name"
}
]
},
mappings: [
{
if: "noname=yes",
"then": "Has no name"
}
],
condition: "x="
});