Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bug/319 localization double parse #349

Merged
merged 8 commits into from
Aug 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions src/dymaptic.GeoBlazor.Core/Components/Symbols/Symbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -176,19 +176,21 @@ public SymbolSerializationRecord(string Type,
[ProtoMember(27)]
public int? YScale { get; init; }

public Symbol FromSerializationRecord()
public Symbol FromSerializationRecord(bool isOutline = false)
{
return Type switch
{
"outline" => new Outline(Color, Width, LineStyle is null ? null : Enum.Parse<LineStyle>(LineStyle!, true)),
"simple-marker" => new SimpleMarkerSymbol(Outline?.FromSerializationRecord() as Outline, Color, Size,
"simple-marker" => new SimpleMarkerSymbol(Outline?.FromSerializationRecord(true) as Outline, Color, Size,
Style is null ? null : Enum.Parse<SimpleMarkerStyle>(Style!, true), Angle, XOffset, YOffset),
"simple-line" => new SimpleLineSymbol(Color, Width, LineStyle is null ? null : Enum.Parse<LineStyle>(LineStyle!, true)),
"simple-fill" => new SimpleFillSymbol(Outline?.FromSerializationRecord() as Outline, Color,
"simple-line" => isOutline
? new Outline(Color, Width, LineStyle is null ? null : Enum.Parse<LineStyle>(LineStyle!, true))
: new SimpleLineSymbol(Color, Width, LineStyle is null ? null : Enum.Parse<LineStyle>(LineStyle!, true)),
"simple-fill" => new SimpleFillSymbol(Outline?.FromSerializationRecord(true) as Outline, Color,
Style is null ? null : Enum.Parse<FillStyle>(Style!, true)),
"picture-marker" => new PictureMarkerSymbol(Url!, Width, Height, Angle, XOffset, YOffset),
"picture-fill" => new PictureFillSymbol(Url!, Width, Height, XOffset, YOffset, XScale, YScale,
Outline?.FromSerializationRecord() as Outline),
Outline?.FromSerializationRecord(true) as Outline),
"text" => new TextSymbol(Text ?? string.Empty, Color, HaloColor, HaloSize,
MapFont?.FromSerializationRecord(), Angle, BackgroundColor, BorderLineColor,
BorderLineSize, HorizontalAlignment is null ? null : Enum.Parse<HorizontalAlignment>(HorizontalAlignment!, true),
Expand Down
21 changes: 18 additions & 3 deletions src/dymaptic.GeoBlazor.Core/JsModuleManager.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using dymaptic.GeoBlazor.Core.Objects;
using Microsoft.JSInterop;
using System.Security.Claims;
using System.Globalization;

namespace dymaptic.GeoBlazor.Core;

Expand All @@ -8,21 +9,35 @@ namespace dymaptic.GeoBlazor.Core;
/// </summary>
public static class JsModuleManager
{
/// <summary>
/// The browser's culture information. Used to deserialize numbers and dates in <see cref="AttributesDictionary"/>.
/// </summary>
public static CultureInfo ClientCultureInfo { get; set; } = CultureInfo.CurrentCulture;

/// <summary>
/// Retrieves the main entry point for the GeoBlazor Core JavaScript module.
/// </summary>
public static async Task<IJSObjectReference> GetArcGisJsCore(IJSRuntime jsRuntime, IJSObjectReference? proModule, CancellationToken cancellationToken)
{
Version? version = System.Reflection.Assembly.GetAssembly(typeof(JsModuleManager))!.GetName().Version;

IJSObjectReference core;

if (proModule is null)
{
return await jsRuntime
core = await jsRuntime
.InvokeAsync<IJSObjectReference>("import", cancellationToken,
$"./_content/dymaptic.GeoBlazor.Core/js/arcGisJsInterop.js?v={version}");
}
else
{
core = await proModule.InvokeAsync<IJSObjectReference>("getCore", cancellationToken);
}

string browserLanguage = await core.InvokeAsync<string>("getBrowserLanguage", cancellationToken);
ClientCultureInfo = new CultureInfo(browserLanguage);

return await proModule.InvokeAsync<IJSObjectReference>("getCore", cancellationToken);
return core;
}

/// <summary>
Expand Down
40 changes: 31 additions & 9 deletions src/dymaptic.GeoBlazor.Core/Objects/AttributesDictionary.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using dymaptic.GeoBlazor.Core.Components;
using ProtoBuf;
using System.Globalization;
using System.Text.Json;
using System.Text.Json.Serialization;

Expand All @@ -13,7 +14,7 @@ namespace dymaptic.GeoBlazor.Core.Objects;
public class AttributesDictionary : IEquatable<AttributesDictionary>
{
/// <summary>
/// Constructor
/// Constructor for a new, empty dictionary
/// </summary>
public AttributesDictionary()
{
Expand All @@ -30,6 +31,7 @@ public AttributesDictionary(Dictionary<string, object?> dictionary)
{
_backingDictionary = new Dictionary<string, object?>();
var options = new JsonSerializerOptions { PropertyNameCaseInsensitive = true };
CultureInfo cultureInfo = JsModuleManager.ClientCultureInfo;

foreach (KeyValuePair<string, object?> kvp in dictionary)
{
Expand All @@ -41,19 +43,23 @@ public AttributesDictionary(Dictionary<string, object?> dictionary)
JsonValueKind.Array => jsonElement.Deserialize(typeof(IEnumerable<object>), options),
JsonValueKind.False => false,
JsonValueKind.True => true,
JsonValueKind.Number => jsonElement.ToString().Contains('.')
? Convert.ChangeType(jsonElement.ToString(), TypeCode.Double)
: Convert.ChangeType(jsonElement.ToString(), TypeCode.Int64),
JsonValueKind.Number => double.Parse(jsonElement.ToString(), cultureInfo),
JsonValueKind.String => jsonElement.ToString(),
_ => jsonElement
};
if (typedValue is string stringValue && Guid.TryParse(stringValue, out Guid guidValue))
if (typedValue is string stringValue)
{
typedValue = guidValue;
if (Guid.TryParse(stringValue, out Guid guidValue))
{
typedValue = guidValue;
}
else if (DateTime.TryParse(stringValue, cultureInfo, DateTimeStyles.None, out DateTime dateValue))
{
typedValue = dateValue;
}
}
_backingDictionary[kvp.Key] = (typedValue ?? default(object?))!;
}

// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
else if (kvp.Value is null) // could be null from serialization
{
Expand All @@ -66,24 +72,39 @@ public AttributesDictionary(Dictionary<string, object?> dictionary)
}
}

/// <summary>
/// Internal constructor for use with Protobuf deserialization
/// </summary>
/// <param name="serializedAttributes">
/// The serialized attributes to use.
/// </param>
internal AttributesDictionary(AttributeSerializationRecord[]? serializedAttributes)
{
_backingDictionary = new Dictionary<string, object?>();

if (serializedAttributes is not null)
{
CultureInfo cultureInfo = JsModuleManager.ClientCultureInfo;

foreach (AttributeSerializationRecord record in serializedAttributes)
{
switch (record.ValueType)
{
case "System.Int32":
_backingDictionary[record.Key] = int.Parse(record.Value!, cultureInfo);

break;
case "System.Double":
case "[object Number]":
_backingDictionary[record.Key] = double.Parse(record.Value!);
_backingDictionary[record.Key] = double.Parse(record.Value!, cultureInfo);

break;
case "System.Boolean":
case "[object Boolean]":
_backingDictionary[record.Key] = bool.Parse(record.Value!);

break;
case "System.String":
case "[object String]":
if (Guid.TryParse(record.Value, out Guid guidValue))
{
Expand All @@ -95,8 +116,9 @@ internal AttributesDictionary(AttributeSerializationRecord[]? serializedAttribut
}

break;
case "System.DateTime":
case "[object Date]":
_backingDictionary[record.Key] = DateTime.Parse(record.Value!);
_backingDictionary[record.Key] = DateTime.Parse(record.Value!, cultureInfo);

break;
default:
Expand Down
6 changes: 5 additions & 1 deletion src/dymaptic.GeoBlazor.Core/Scripts/arcGisJsInterop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,7 @@ export async function buildMapView(id: string, dotNetReference: any, long: numbe
}

if (hasValue(popupEnabled)) {
view.popupEnabled = popupEnabled;
view.popupEnabled = popupEnabled as boolean;
}

if (hasValue(constraints)) {
Expand Down Expand Up @@ -3339,4 +3339,8 @@ export function getWebMapBookmarks(viewId: string) {
export function setStretchTypeForRenderer(rendererId, stretchType) {
let renderer = arcGisObjectRefs[rendererId] as RasterStretchRenderer;
renderer.stretchType = stretchType;
}

export function getBrowserLanguage(): string {
return navigator.language;
}
4 changes: 0 additions & 4 deletions src/dymaptic.GeoBlazor.Core/Scripts/dotNetBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,17 +99,14 @@ import Domain from "@arcgis/core/layers/support/Domain";
import CodedValueDomain from "@arcgis/core/layers/support/CodedValueDomain";
import InheritedDomain from "@arcgis/core/layers/support/InheritedDomain";
import RangeDomain from "@arcgis/core/layers/support/RangeDomain";
import Effect = __esri.Effect;
import TileInfo from "@arcgis/core/layers/support/TileInfo";
import LOD from "@arcgis/core/layers/support/LOD";
import LayerSearchSource from "@arcgis/core/widgets/Search/LayerSearchSource";
import SearchSource from "@arcgis/core/widgets/Search/SearchSource";
import LocatorSearchSource from "@arcgis/core/widgets/Search/LocatorSearchSource";
import SearchResult = __esri.SearchResult;
import SuggestResult = __esri.SuggestResult;
import FeatureType from "@arcgis/core/layers/support/FeatureType";
import FeatureTemplate from "@arcgis/core/layers/support/FeatureTemplate";
import FeatureTemplateThumbnail = __esri.FeatureTemplateThumbnail;
import LabelClass from "@arcgis/core/layers/support/LabelClass";
import Renderer from "@arcgis/core/renderers/Renderer";
import PieChartRenderer from "@arcgis/core/renderers/PieChartRenderer";
Expand All @@ -120,7 +117,6 @@ import PieChartScheme = __esri.PieChartScheme;
import SizeScheme = __esri.SizeScheme;
import SizeSchemeForPoint = __esri.SizeSchemeForPoint;
import SizeSchemeForPolygon = __esri.SizeSchemeForPolygon;
import UniqueValuesResult = __esri.UniqueValuesResult;
import AuthoringInfo from "@arcgis/core/renderers/support/AuthoringInfo";
import AddressCandidate = __esri.AddressCandidate;
import FeatureSet from "@arcgis/core/rest/support/FeatureSet";
Expand Down
2 changes: 2 additions & 0 deletions src/dymaptic.GeoBlazor.Core/dymaptic.GeoBlazor.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Sample.Shared" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Test" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Pro.Test.Unit" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Core.Test.Blazor.Shared" />
<InternalsVisibleTo Include="dymaptic.GeoBlazor.Pro.Test.Blazor.Shared" />
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
using dymaptic.GeoBlazor.Core.Objects;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System.Globalization;
using System.Runtime.Serialization;


namespace dymaptic.GeoBlazor.Core.Test.Blazor.Shared.Components;

public class LocalizationTests: TestRunnerBase
{
[TestMethod]
public void TestCanDeserializeAmericanDoubleAttribute()
{
string originalCulture = JsModuleManager.ClientCultureInfo.Name;
SetCulture("en-US");
AttributeSerializationRecord doubleAttribute = new("Value", "1.1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(1.1, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestCanDeserializeEuropeanDoubleAttribute()
{
string originalCulture = JsModuleManager.ClientCultureInfo.Name;
SetCulture("de-DE");
AttributeSerializationRecord doubleAttribute = new("Value", "1,1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(1.1, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestDeserializeAmericanDoubleAttributeInEuropeanCultureReturnsWrongValue()
{
string originalCulture = JsModuleManager.ClientCultureInfo.Name;
SetCulture("de-DE");
AttributeSerializationRecord doubleAttribute = new("Value", "1.1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(11.0, attributes["Value"]);
SetCulture(originalCulture);
}

[TestMethod]
public void TestDeserializeEuropeanDoubleAttributeInAmericanCultureReturnsWrongValue()
{
string originalCulture = JsModuleManager.ClientCultureInfo.Name;
SetCulture("en-US");
AttributeSerializationRecord doubleAttribute = new("Value", "1,1", "[object Number]");
AttributesDictionary attributes = new([doubleAttribute]);
Assert.AreEqual(11.0, attributes["Value"]);
SetCulture(originalCulture);
}

private void SetCulture(string cultureName)
{
JsModuleManager.ClientCultureInfo = new CultureInfo(cultureName);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -177,14 +177,29 @@
try
{
bool hasParameters = methodInfo.GetParameters().Any();
if (hasParameters)
if (methodInfo.ReturnType != typeof(Task))
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
await (Task)methodInfo.Invoke(this, [(Action)RenderHandler])!;
if (hasParameters)
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
methodInfo.Invoke(this, [(Action)RenderHandler]);
}
else
{
methodInfo.Invoke(this, null);
}
}
else
{
await (Task)methodInfo.Invoke(this, null)!;
if (hasParameters)
{
void RenderHandler() => _methodsWithRenderedMaps.Add(methodInfo.Name);
await (Task)methodInfo.Invoke(this, [(Action)RenderHandler])!;
}
else
{
await (Task)methodInfo.Invoke(this, null)!;
}
}

_passed[methodInfo.Name] = _resultBuilder.ToString();
Expand Down
31 changes: 30 additions & 1 deletion test/dymaptic.GeoBlazor.Core.Test/SerializationUnitTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,9 @@ public void RoundTripSerializeGraphicToJson()
Assert.AreEqual(((SimpleMarkerSymbol)graphic.Symbol!).Outline!.Color, ((SimpleMarkerSymbol)deserialized.Symbol!).Outline!.Color);
Assert.AreEqual(((SimpleMarkerSymbol)graphic.Symbol!).Size, ((SimpleMarkerSymbol)deserialized.Symbol!).Size);
Assert.AreEqual(graphic.PopupTemplate!.Title, deserialized.PopupTemplate!.Title);

Assert.AreEqual(graphic.Attributes.Count, deserialized.Attributes.Count);
Assert.AreEqual(graphic.Attributes["testString"], deserialized.Attributes["testString"]);
Assert.AreEqual(graphic.Attributes["testNumber"], deserialized.Attributes["testNumber"]);
}

[TestMethod]
Expand All @@ -78,6 +80,33 @@ public void SerializeToProtobuf()
Console.WriteLine($"SerializeGraphicToJson: {sw.ElapsedMilliseconds}ms");
Console.WriteLine($"Size: {data.Length} bytes");
}

[TestMethod]
public void RoundTripSerializeToProtobuf()
{
var graphic = new Graphic(new Point(_random.NextDouble() * 10 + 11.0,
_random.NextDouble() * 10 + 50.0),
new SimpleMarkerSymbol(new Outline(new MapColor("green")), new MapColor("red"), 10),
new PopupTemplate("Test", "Test Content<br/>{testString}<br/>{testNumber}", new[] { "*" }),
new AttributesDictionary(
new Dictionary<string, object?> { { "testString", "test" }, { "testNumber", 123 } }));
ProtoGraphicCollection collection = new(new[] { graphic.ToSerializationRecord() });
using MemoryStream ms = new();
Serializer.Serialize(ms, collection);
byte[] data = ms.ToArray();
ProtoGraphicCollection deserializedCollection =
Serializer.Deserialize<ProtoGraphicCollection>((ReadOnlyMemory<byte>)data);
Graphic deserialized = deserializedCollection.Graphics[0].FromSerializationRecord();
Assert.AreEqual(((Point)graphic.Geometry!).Latitude, ((Point)deserialized.Geometry!).Latitude);
Assert.AreEqual(((Point)graphic.Geometry!).Longitude, ((Point)deserialized.Geometry!).Longitude);
Assert.AreEqual(((SimpleMarkerSymbol)graphic.Symbol!).Color, ((SimpleMarkerSymbol)deserialized.Symbol!).Color);
Assert.AreEqual(((SimpleMarkerSymbol)graphic.Symbol!).Outline!.Color, ((SimpleMarkerSymbol)deserialized.Symbol!).Outline!.Color);
Assert.AreEqual(((SimpleMarkerSymbol)graphic.Symbol!).Size, ((SimpleMarkerSymbol)deserialized.Symbol!).Size);
Assert.AreEqual(graphic.PopupTemplate!.Title, deserialized.PopupTemplate!.Title);
Assert.AreEqual(graphic.Attributes.Count, deserialized.Attributes.Count);
Assert.AreEqual(graphic.Attributes["testString"], deserialized.Attributes["testString"]);
Assert.AreEqual(graphic.Attributes["testNumber"], deserialized.Attributes["testNumber"]);
}

private readonly Random _random = new();
}
Loading