Fix all the bugs, feature-complete with the non-refactored version

This commit is contained in:
Pieter Vander Vennet 2020-10-30 00:56:46 +01:00
parent 5adc355a48
commit 25f2aa8e92
11 changed files with 467 additions and 99 deletions

View file

@ -1,5 +1,4 @@
import {Layout} from "./Layout";
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";
@ -15,6 +14,7 @@ import * as bike_monitoring_stations from "../assets/themes/bike_monitoring_stat
import * as fritures from "../assets/themes/fritures/fritures.json"
import * as benches from "../assets/themes/benches/benches.json";
import * as charging_stations from "../assets/themes/charging_stations/charging_stations.json"
import * as widths from "../assets/themes/widths/width.json"
import {PersonalLayout} from "../Logic/PersonalLayout";
import LayerConfig from "./JSON/LayerConfig";
@ -41,11 +41,20 @@ export class AllKnownLayouts {
}
private static GenerateWidths(): Layout {
const layout = Layout.LayoutFromJSON(widths, SharedLayers.sharedLayers);
layout.enableUserBadge = false;
return layout;
}
private static GenerateBuurtNatuur(): Layout {
const layout = Layout.LayoutFromJSON(buurtnatuur, SharedLayers.sharedLayers);
layout.enableMoreQuests = false;
layout.enableShareScreen = false;
layout.hideFromOverview = true;
console.log("Buurtnatuur:",layout)
return layout;
}
@ -73,6 +82,7 @@ export class AllKnownLayouts {
Layout.LayoutFromJSON(fritures, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(benches, SharedLayers.sharedLayers),
Layout.LayoutFromJSON(charging_stations, SharedLayers.sharedLayers),
AllKnownLayouts.GenerateWidths(),
AllKnownLayouts.GenerateBuurtNatuur(),
AllKnownLayouts.GenerateBikeMonitoringStations(),

View file

@ -1,8 +1,7 @@
import Translation from "../../UI/i18n/Translation";
import Translations, {Translation} from "../../UI/i18n/Translations";
import TagRenderingConfig from "./TagRenderingConfig";
import {Tag, TagsFilter} from "../../Logic/Tags";
import {LayerConfigJson} from "./LayerConfigJson";
import Translations from "../../UI/i18n/Translations";
import {FromJSON} from "./FromJSON";
import SharedTagRenderings from "../SharedTagRenderings";
import {TagRenderingConfigJson} from "./TagRenderingConfigJson";
@ -25,6 +24,7 @@ export default class LayerConfig {
iconSize: TagRenderingConfig;
color: TagRenderingConfig;
width: TagRenderingConfig;
dashArray: TagRenderingConfig;
wayHandling: number;
@ -54,11 +54,12 @@ export default class LayerConfig {
this.wayHandling = json.wayHandling ?? 0;
this.hideUnderlayingFeaturesMinPercentage = json.hideUnderlayingFeaturesMinPercentage ?? 0;
this.title = new TagRenderingConfig(json.title);
this.presets = (json.presets ?? []).map(pr => ({
title: Translations.T(pr.title),
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
description: Translations.T(pr.description)
}))
this.presets = (json.presets ?? []).map(pr =>
({
title: Translations.T(pr.title),
tags: pr.tags.map(t => FromJSON.SimpleTag(t)),
description: Translations.T(pr.description)
}))
/**
@ -108,6 +109,7 @@ export default class LayerConfig {
this.iconSize = tr("iconSize", "40,40,center");
this.color = tr("color", "#0000ff");
this.width = tr("width", "7");
this.dashArray = tr("dashArray", "");
}

View file

@ -64,6 +64,13 @@ export interface LayerConfigJson {
*/
width?: string | TagRenderingConfigJson;
/**
* A dasharray, e.g. "5 6"
* The dasharray defines 'pixels of line, pixels of gap, pixels of line, pixels of gap',
* Default value: "" (empty string == full line)
*/
dashArray?: string | TagRenderingConfigJson
/**
* Wayhandling: should a way/area be displayed as:
* 0) The way itself

View file

@ -57,6 +57,9 @@ export class FilteredLayer {
const iconUrl = layerDef.icon?.GetRenderValue(tags)?.txt ?? "./assets/bug.svg";
const iconSize = (layerDef.iconSize?.GetRenderValue(tags)?.txt ?? "40,40,center").split(",");
const dashArray = layerDef.dashArray.GetRenderValue(tags)?.txt.split(" ").map(Number);
function num(str, deflt = 40) {
const n = Number(str);
if (isNaN(n)) {
@ -97,7 +100,8 @@ export class FilteredLayer {
popupAnchor: [0, 3 - anchorH]
},
color: color,
weight: weight
weight: weight,
dashArray: dashArray
};
};
this.name = name;

View file

@ -2,6 +2,8 @@ import {GeoOperations} from "./GeoOperations";
import CodeGrid from "./Web/CodeGrid";
import State from "../State";
import opening_hours from "opening_hours";
import {And, Or, Tag} from "./Tags";
import {Utils} from "../Utils";
class SimpleMetaTagger {
@ -40,90 +42,201 @@ class SimpleMetaTagger {
export default class MetaTagging {
private static latlon = new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)",
(feature => {
const centerPoint = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0];
feature.properties["_lat"] = "" + lat;
feature.properties["_lon"] = "" + lon;
})
);
private static surfaceArea = new SimpleMetaTagger(
["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways",
(feature => {
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
feature.properties["_surface"] = "" + sqMeters;
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
})
);
private static country = new SimpleMetaTagger(
["_country"], "The country code of the point",
((feature, index) => {
const centerPoint = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0]
// But the codegrid SHOULD be a number!
CodeGrid.getCode(lat, lon, (error, code) => {
if (error === null) {
feature.properties["_country"] = code;
// There is a huge performance issue: if there are ~1000 features receiving a ping at the same time,
// The application hangs big time
// So we disable pinging all together
} else {
console.warn("Could not determine country for", feature.properties.id, error);
}
});
})
)
private static isOpen = new SimpleMetaTagger(
["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no",
(feature => {
const tagsSource = State.state.allElements.addOrGetElement(feature);
tagsSource.addCallback(tags => {
if (tags["opening_hours"] !== undefined && tags["_country"] !== undefined) {
if (tags._isOpen !== undefined) {
// Already defined
return;
}
const oh = new opening_hours(tags["opening_hours"], {
lat: tags._lat,
lon: tags._lon,
address: {
country_code: tags._country
}
}, {tag_key: "opening_hours"});
const updateTags = () => {
tags["_isOpen"] = oh.getState() ? "yes" : "no";
const comment = oh.getComment();
if (comment) {
tags["_isOpen:description"] = comment;
}
const nextChange = oh.getNextChange() as Date;
if (nextChange !== undefined) {
window.setTimeout(
updateTags,
(nextChange.getTime() - (new Date()).getTime())
)
}
}
updateTags();
}
})
})
)
public static carriageWayWidth = new SimpleMetaTagger(
["_width:needed","_width:needed:no_pedestrians", "_width:difference"],
"Legacy for a specific project calculating the needed width for safe traffic on a road",
(feature: any, index: number) => {
const carWidth = 2;
const cyclistWidth = 1.5;
const pedestrianWidth = 0.75;
const properties = feature.properties;
const _leftSideParking =
new And([new Tag("parking:lane:left", "parallel"), new Tag("parking:lane:right", "no_parking")]);
const _rightSideParking =
new And([new Tag("parking:lane:right", "parallel"), new Tag("parking:lane:left", "no_parking")]);
const _bothSideParking = new Tag("parking:lane:both", "parallel");
const _noSideParking = new Tag("parking:lane:both", "no_parking");
const _otherParkingMode =
new Or([
new Tag("parking:lane:both", "perpendicular"),
new Tag("parking:lane:left", "perpendicular"),
new Tag("parking:lane:right", "perpendicular"),
new Tag("parking:lane:both", "diagonal"),
new Tag("parking:lane:left", "diagonal"),
new Tag("parking:lane:right", "diagonal"),
])
const _sidewalkBoth = new Tag("sidewalk", "both");
const _sidewalkLeft = new Tag("sidewalk", "left");
const _sidewalkRight = new Tag("sidewalk", "right");
const _sidewalkNone = new Tag("sidewalk", "none");
let parkingStateKnown = true;
let parallelParkingCount = 0;
const _oneSideParking = new Or([_leftSideParking, _rightSideParking]);
if (_oneSideParking.matchesProperties(properties)) {
parallelParkingCount = 1;
} else if (_bothSideParking.matchesProperties(properties)) {
parallelParkingCount = 2;
} else if (_noSideParking.matchesProperties(properties)) {
parallelParkingCount = 0;
} else if (_otherParkingMode.matchesProperties(properties)) {
parallelParkingCount = 0;
} else {
parkingStateKnown = false;
console.log("No parking data for ", properties.name, properties.id, properties)
}
let pedestrianFlowNeeded;
if (_sidewalkBoth.matchesProperties(properties)) {
pedestrianFlowNeeded = 0;
} else if (_sidewalkNone.matchesProperties(properties)) {
pedestrianFlowNeeded = 2;
} else if (_sidewalkLeft.matchesProperties(properties) || _sidewalkRight.matchesProperties(properties)) {
pedestrianFlowNeeded = 1;
} else {
pedestrianFlowNeeded = -1;
}
let onewayCar = properties.oneway === "yes";
let onewayBike = properties["oneway:bicycle"] === "yes" ||
(onewayCar && properties["oneway:bicycle"] === undefined)
let cyclingAllowed =
!(properties.bicycle === "use_sidepath"
|| properties.bicycle === "no");
let carWidthUsed = (onewayCar ? 1 : 2) * carWidth;
properties["_width:needed:cars"] = Utils.Round(carWidthUsed);
properties["_width:needed:parking"] = Utils.Round(parallelParkingCount * carWidth)
let cyclistWidthUsed = 0;
if (cyclingAllowed) {
cyclistWidthUsed = (onewayBike ? 1 : 2) * cyclistWidth;
}
properties["_width:needed:cyclists"] = Utils.Round(cyclistWidthUsed)
const width = parseFloat(properties["width:carriageway"]);
const targetWidthIgnoringPedestrians =
carWidthUsed +
cyclistWidthUsed +
parallelParkingCount * carWidthUsed;
properties["_width:needed:no_pedestrians"] =Utils.Round(targetWidthIgnoringPedestrians);
const pedestriansNeed = Math.max(0, pedestrianFlowNeeded) * pedestrianWidth;
const targetWidth = targetWidthIgnoringPedestrians + pedestriansNeed ;
properties["_width:needed"] = Utils.Round(targetWidth);
properties["_width:needed:pedestrians"] = Utils.Round(pedestriansNeed)
properties["_width:difference"] = Utils.Round(targetWidth - width );
properties["_width:difference:no_pedestrians"] = Utils.Round(targetWidthIgnoringPedestrians - width) ;
}
);
public static metatags = [
new SimpleMetaTagger(["_lat", "_lon"], "The latitude and longitude of the point (or centerpoint in the case of a way/area)",
(feature => {
const centerPoint = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0];
feature.properties["_lat"] = "" + lat;
feature.properties["_lon"] = "" + lon;
})
),
new SimpleMetaTagger(
["_surface", "_surface:ha"], "The surface area of the feature, in square meters and in hectare. Not set on points and ways",
(feature => {
const sqMeters = GeoOperations.surfaceAreaInSqMeters(feature);
feature.properties["_surface"] = "" + sqMeters;
feature.properties["_surface:ha"] = "" + Math.floor(sqMeters / 1000) / 10;
MetaTagging.latlon,
MetaTagging.surfaceArea,
MetaTagging.country,
MetaTagging.isOpen,
MetaTagging.carriageWayWidth
})
),
new SimpleMetaTagger(
["_country"], "The country code of the point",
((feature, index) => {
const centerPoint = GeoOperations.centerpoint(feature);
const lat = centerPoint.geometry.coordinates[1];
const lon = centerPoint.geometry.coordinates[0]
// But the codegrid SHOULD be a number!
CodeGrid.getCode(lat, lon, (error, code) => {
if (error === null) {
feature.properties["_country"] = code;
// There is a huge performance issue: if there are ~1000 features receiving a ping at the same time,
// The application hangs big time
// So we disable pinging all together
} else {
console.warn("Could not determine country for", feature.properties.id, error);
}
});
})
),
new SimpleMetaTagger(
["_isOpen", "_isOpen:description"], "If 'opening_hours' is present, it will add the current state of the feature (being 'yes' or 'no",
(feature => {
const tagsSource = State.state.allElements.addOrGetElement(feature);
tagsSource.addCallback(tags => {
if (tags["opening_hours"] !== undefined && tags["_country"] !== undefined) {
if (tags._isOpen !== undefined) {
// Already defined
return;
}
const oh = new opening_hours(tags["opening_hours"], {
lat: tags._lat,
lon: tags._lon,
address: {
country_code: tags._country
}
}, {tag_key: "opening_hours"});
const updateTags = () => {
tags["_isOpen"] = oh.getState() ? "yes" : "no";
const comment = oh.getComment();
if (comment) {
tags["_isOpen:description"] = comment;
}
const nextChange = oh.getNextChange() as Date;
if (nextChange !== undefined) {
window.setTimeout(
updateTags,
(nextChange.getTime() - (new Date()).getTime())
)
}
}
updateTags();
}
})
})
)
];
static addMetatags(features: any[]) {

View file

@ -15,9 +15,9 @@ import {TagRenderingConfigJson} from "../../Customizations/JSON/TagRenderingConf
import {UserDetails} from "../../Logic/Osm/OsmConnection";
import State from "../../State";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FromJSON} from "../../Customizations/JSON/FromJSON";
import ValidatedTextField from "../Input/ValidatedTextField";
import SpecialVisualizations from "../SpecialVisualizations";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
export default class TagRenderingPanel extends InputElement<TagRenderingConfigJson> {
@ -111,7 +111,7 @@ export default class TagRenderingPanel extends InputElement<TagRenderingConfigJs
this.validText = new VariableUiElement(value.map((json: TagRenderingConfigJson) => {
try{
FromJSON.TagRendering(json, options?.title ?? "");
new TagRenderingConfig(json, options?.title ?? "");
return "";
}catch(e){
return "<span class='alert'>"+e+"</span>"

View file

@ -2,9 +2,10 @@ import {UIElement} from "../UIElement";
import {UIEventSource} from "../../Logic/UIEventSource";
import TagRenderingPanel from "./TagRenderingPanel";
import {VariableUiElement} from "../Base/VariableUIElement";
import {FromJSON} from "../../Customizations/JSON/FromJSON";
import {FixedUiElement} from "../Base/FixedUiElement";
import Combine from "../Base/Combine";
import TagRenderingConfig from "../../Customizations/JSON/TagRenderingConfig";
import EditableTagRendering from "../Popup/EditableTagRendering";
export default class TagRenderingPreview extends UIElement {
@ -39,8 +40,7 @@ export default class TagRenderingPreview extends UIElement {
rendering =
new VariableUiElement(es.map(tagRenderingConfig => {
try {
const tr = FromJSON.TagRendering(tagRenderingConfig, "preview")
.construct(self.previewTagValue);
const tr = new EditableTagRendering(self.previewTagValue, new TagRenderingConfig(tagRenderingConfig, "preview"));
return tr.Render();
} catch (e) {
return new Combine(["Could not show this tagrendering:", e.message]).Render();

View file

@ -49,7 +49,9 @@ export class SimpleAddUI extends UIElement {
this.ListenTo(layer.isDisplayed);
for (const preset of layer.layerDef.presets) {
const presets = layer.layerDef.presets;
for (const preset of presets) {
console.log("Preset:", preset)
let icon: string = layer.layerDef.icon.GetRenderValue(
TagUtils.KVtoProperties(preset.tags ?? [])).txt ??

View file

@ -39,6 +39,17 @@ export class Utils {
return "" + i;
}
public static Round(i: number) {
if(i < 0){
return "-" + Utils.Round(-i);
}
const j = "" + Math.floor(i * 10);
if (j.length == 1) {
return "0." + j;
}
return j.substr(0, j.length - 1) + "." + j.substr(j.length - 1, j.length);
}
public static Times(f: ((i: number) => string), count: number): string {
let res = "";
for (let i = 0; i < count; i++) {

View file

@ -0,0 +1,204 @@
{
"id": "width",
"title": {
"nl": "Straatbreedtes"
},
"shortDescription": {
"nl": "Is de straat breed genoeg?"
},
"description": {
"nl": " <h3>De straat is opgebruikt</h3>\n <p>Er is steeds meer druk op de openbare ruimte. Voetgangers, fietsers, steps, auto's, bussen, bestelwagens, buggies, cargobikes, ... willen allemaal hun deel van de openbare ruimte.</p>\n <p>In deze studie nemen we Brugge onder de loep en kijken we hoe breed elke straat is én hoe breed elke straat zou moeten zijn voor een veilig én vlot verkeer.</p>\n <h3>Legende</h3>\n <span style='background: red'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat te smal voor veilig verkeer<br/>\n <span style='background: #0f0'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat is breed genoeg veilig verkeer<br/>\n <span style='background: orange'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Straat zonder voetpad, te smal als ook voetgangers plaats krijgen<br/>\n <span style='background: lightgrey'>&NonBreakingSpace;&NonBreakingSpace;&NonBreakingSpace;</span> Woonerf, autoluw, autoloos of enkel plaatselijk verkeer<br/>\n <br/>\n <br/>\n Een gestippelde lijn is een straat waar ook voor fietsers éénrichtingsverkeer geldt.<br/>\n Klik op een straat om meer informatie te zien.\n <h3>Hoe gaan we verder?</h3>\n Verschillende ingrepen kunnen de stad teruggeven aan de inwoners en de stad leefbaarder en levendiger maken.<br/>\n Denk aan:\n <ul>\n <li>De autovrije zone's uitbreiden</li>\n <li>De binnenstad fietszone maken</li>\n <li>Het aantal woonerven uitbreiden</li>\n <li>Grotere auto's meer belasten - ze nemen immers meer parkeerruimte in.</li>\n <li>Laat toeristen verplicht parkeren onder het zand; een (fiets)taxi kan hen naar hun hotel brengen</li>\n <li>Voorzie in elke straat enkele parkeerplaatsen voor kortparkeren. Zo kunnen leveringen, iemand afzetten,... gebeuren zonder op het voetpad en fietspad te parkeren</li>\n </ul>\""
},
"language": [
"nl"
],
"maintainer": "",
"icon": "./assets/themes/widths/icon.svg",
"version": "0",
"startLat": 51.20875,
"startLon": 3.22435,
"startZoom": 14,
"widenFactor": 0.05,
"socialImage": "",
"layers": [
{
"id": "widths",
"name": {
"nl": "Straten met een breedte"
},
"minzoom": 14,
"overpassTags": {
"and": [
"width:carriageway~*"
]
},
"titleIcons": [],
"title": {
"render": {
"nl": "{name}"
},
"condition": {
"and": []
},
"mappings": [
{
"if": {
"and": [
"name="
]
},
"then": {
"nl": "Naamloos segmet"
}
}
]
},
"description": {},
"tagRenderings": [
{
"render": "Deze straat is <b>{width:carriageway}m</b> breed"
},
{
"render": "Deze straat heeft <span class='alert'>{_width:difference}m</span> te weinig:",
"mappings": [
{
"if": {
"or": [
"_width:difference~-.*",
"_width:difference=0.0"
]
},
"then": "Deze straat is breed genoeg:"
}
]
},
{
"render": "<b>{_width:needed:cars}m</b> voor het autoverkeer",
"mappings": [
{
"if": "oneway=yes",
"then": "<b>{_width:needed:cars}m</b> voor het éénrichtings-autoverkeer"
},
{
"if": "oneway=no",
"then": "<b>{_width:needed:cars}m</b> voor het tweerichtings-autoverkeer"
}
]
},
{
"render": "<b>{_width:needed:parking}m</b> voor het geparkeerde wagens",
"condition": "_width:needed:parking!=0.0"
},
{
"render": "<b>{_width:needed:cyclists}m</b> voor fietsers",
"mappings": [
{
"if": "bicycle=use_sidepath",
"then": "Fietsers hebben hier een vrijliggend fietspad en worden dus niet meegerekend"
},
{
"if": "oneway:bicycle=yes",
"then": "<b>{_width:needed:cyclists}m</b> voor fietsers, die met de rijrichting mee moeten"
}
]
},
{
"render": "<b>{_width:needed:pedestrians}m</b> voor voetgangers",
"condition": "_width:needed:pedestrians!=0.0",
"mappings": [
{
"if": {
"or": [
"sidewalk=none",
"sidewalk=no"
]
},
"then": "<b>{_width:needed:pedestrians}m</b> voor voetgangers: er zijn hier geen voetpaden"
},
{
"if": {
"or": [
"sidewalk=left",
"sidewalk=right"
]
},
"then": "<b>{_width:needed:pedestrians}m</b> voor voetgangers: er is slechts aan één kant een voetpad"
}
]
},
{
"render": "<span style='border: 1px solid black; border-radius: 0.5em; padding: 0.25em;'><b>{_width:needed}m</b> nodig in het totaal</span>"
},
{
"render": "{all_tags()}"
}
],
"hideUnderlayingFeaturesMinPercentage": 0,
"icon": {
"render": "./assets/themes/widths/icon.svg"
},
"width": {
"render": "4"
},
"iconSize": {
"render": "40,40,center"
},
"color": {
"render": "#00f",
"mappings": [
{
"if": {
"or": [
"access=destination",
"highway=living_street",
"highway=pedestrian",
"motor_vehicle=no",
"motor_vehicle=destination"
]
},
"then": "lightgrey"
},
{
"if": "_width:difference~-.*",
"then": "#0f0"
},
{
"if": {
"and": [
"_width:difference!~-.*",
"_width:difference:no_pedestrians~-.*"
]
},
"then": "orange"
},
{
"if": "_width:difference!~-.*",
"then": "#f00"
}
]
},
"dashArray": {
"render": "",
"mappings": [
{
"if": {
"and": [
"oneway=yes",
{
"or": [
"oneway:bicycle=yes",
"oneway:bicycle="
]
}
]
},
"then": "5 6"
}
]
},
"presets": []
}
],
"roamingRenderings": [],
"defaultBackgroundId": "Stadia.AlidadeSmoothDark"
}

View file

@ -13,6 +13,7 @@ import PublicHolidayInput from "../UI/Input/OpeningHours/PublicHolidayInput";
import TagRenderingConfig from "../Customizations/JSON/TagRenderingConfig";
import EditableTagRendering from "../UI/Popup/EditableTagRendering";
import {SubstitutedTranslation} from "../UI/SpecialVisualizations";
import {Utils} from "../Utils";
@ -323,5 +324,19 @@ new T([
["OH Parse PH 12:00-17:00", () => {
const rules = PublicHolidayInput.LoadValue("PH 12:00-17:00");
equal(rules.mode, " ");
}],
["Round", () => {
equal(Utils.Round(15), "15.0")
equal(Utils.Round(1), "1.0")
equal(Utils.Round(1.5), "1.5")
equal(Utils.Round(0.5), "0.5")
equal(Utils.Round(1.6), "1.6")
equal(Utils.Round(-15), "-15.0")
equal(Utils.Round(-1), "-1.0")
equal(Utils.Round(-1.5), "-1.5")
equal(Utils.Round(-0.5), "-0.5")
equal(Utils.Round(-1.6), "-1.6")
}]
]);