I am using the AzureMapsControl component which wraps Azure Maps. What I do is pretty simple just putting pins up. At times, when scrolling, I get a ton of the following error messages in the browser (client). Not clicking, just scrolling (video example).
atlas.min.js:55 Error: Could not load image because of The source image could not be decoded.. Please make sure to use a supported image type such as PNG or JPEG. Note that SVGs are not supported.
at atlas.min.js:56:190132
Any idea what's going on and what I can do to avoid this?
Update:
I simplified my code a lot (below) and still get this. I set a breakpoint in the browser and here's the error:
ue.arrayBufferToImageBitmap = function(e, t)
And here's the parameters:
e: ArrayBuffer(0)
byteLength: 0
detached: false
maxByteLength: 0
resizable: false
t: (e,t)=> {…}
length: 2
name: "n"
arguments: (...)
caller: (...)
And here's the key part of the call stack (lots of minimized calls not listed):
e.getImage
loadTile
_loadTile
_addTile
_updateRetainedTiles
update
_updateSources
_render
And here's the full code. I've reduced it to just setting the image to pin-blue so there's no issue with checking for type. And if I remove setting OnSourceAdded="OnDatasourceAdded" the problem goes away. So it is an issue with rendering the pin. But I have no custom pin images.
Is it possible that I need to load pin-blue? They display fine so I think they're being loaded.
SearchMap.razor
<AzureMap Id="map"
@ref="MyMap"
CameraOptions="new CameraOptions { Zoom = MapZoom }"
Controls="new Control[]
{
new ZoomControl(new ZoomControlOptions { Style = ControlStyle.Auto }, ControlPosition.TopLeft),
new CompassControl(new CompassControlOptions { Style = ControlStyle.Auto }, ControlPosition.BottomLeft),
new StyleControl(new StyleControlOptions { Style = ControlStyle.Auto, MapStyles = MapStyle.All() }, ControlPosition.BottomRight)
}"
StyleOptions='new StyleOptions { ShowLogo = false, Language = "en-US" }'
EventActivationFlags="MapEventActivationFlags
.None()
.Enable(MapEventType.Ready, MapEventType.SourceAdded,
MapEventType.Click, MapEventType.MoveEnd)"
OnReady="OnMapReadyAsync"
OnSourceAdded="OnDatasourceAdded"
OnMoveEnd="OnMapMoveEnd"/>
SearchMap.razor.cs - it's long and I think only OnDatasourceAdded() matters. But I figure better to list it all so there's no assumptions about what is being set up.
using System.Text.Json;
using LouisHowe.web.PageModels.EventJob;
using LouisHowe.web.PageModels.Org;
using LouisHowe.web.PageModels.Shared;
using Microsoft.AspNetCore.Components;
using AzureMapsControl.Components.Atlas;
using AzureMapsControl.Components.Data;
using AzureMapsControl.Components.Layers;
using AzureMapsControl.Components.Map;
using AzureMapsControl.Components.Popups;
using Darnton.Blazor.DeviceInterop.Geolocation;
using TopologyPoint = NetTopologySuite.Geometries.Point;
using TopologyPolygon = NetTopologySuite.Geometries.Polygon;
using AtlasMapPoint = AzureMapsControl.Components.Atlas.Point;
using Position = AzureMapsControl.Components.Atlas.Position;
using NetTopologySuite.Geometries;
namespace LouisHowe.web.Pages.User
{
public partial class SearchMap : ExComponentBase
{
/// <summary>
/// The initial zoom value for the map.
/// </summary>
public static int DefaultZoomLevel = 12;
/// <summary>
/// How many degrees of longitude (X) in 1 mile.
/// </summary>
public static double LongitudeOneMile = 0.0181818181818182;
/// <summary>
/// How many degrees of latitude (Y) in 1 mile.
/// </summary>
public static double LatitudeOneMile = 0.0144927536231884;
[Inject]
IGeolocationService GeolocationService { get; set; } = default!;
/// <summary>
/// The data for the event grid.
/// </summary>
[Parameter]
public SearchMapPageModel? Data { get; set; }
/// <summary>
/// The "working" center of the map. At startup, it will set this to, in order: the browser's current position,
/// The User.Address, the Washington Monument.<br/>
/// Use X for longitude and Y for latitude.
/// </summary>
[Parameter]
public TopologyPoint? MapCenter { get; set; }
/// <summary>
/// The initial zoom level for the map.
/// </summary>
[Parameter]
public double? MapZoom { get; set; } = DefaultZoomLevel;
/// <summary>
/// Called when the map center or extent has changed. This will not be called on a move of less than 10 miles
/// or a zoom in (or zoom back out to the original zoom). In short, this is called when the map display has
/// changed enough that the search should be re-run.<br/>
/// This will also be called when the map is first displayed.<br/>
/// Passes (center, extent, zoom, runSearch). The extent passed in this is larger by about 10 miles over the
/// actual map display. This is why it can pass runSearch== false for drags of under 5 miles.<br/>
/// In Point and Polygon use X for longitude and Y for latitude.
/// </summary>
[Parameter]
public EventCallback<(TopologyPoint, TopologyPolygon, double, bool)> MapMoved { get; set; }
/// <summary>
/// Passed to Signup in case user is not logged in.
/// </summary>
[Parameter]
public string? ReturnUrl { get; set; }
/// <summary>
/// false if OnMapReadyAsync() has not been called yet. true once it has been called and completed.
/// </summary>
private bool _firstMapReadyComplete;
/// <summary>
/// The name of the datasource.
/// </summary>
private string EntitiesDataSourceId => "EventsAndOrgs";
/// <summary>
/// The datasource for the events.
/// </summary>
private DataSource? EntitiesDataSource { get; set; }
/// <summary>
/// The "type" property for an event.
/// </summary>
private static string _typeEvent = "Event";
/// <summary>
/// The "type" property for an organization.
/// </summary>
private static string _typeOrganization = "Organization";
/// <summary>
/// The popup for the pin the mouse is over.
/// </summary>
private Popup? PinTooltip { get; set; }
/// <summary>
/// The map component.
/// </summary>
private AzureMap MyMap { get; set; } = default!;
/// <summary>
/// true to show the event popup, false to hide it.
/// </summary>
public bool EventPopupVisible { get; set; }
/// <summary>
/// The header text for the event popup.
/// </summary>
public string? EventPopupHeader { get; set; }
/// <summary>
/// The data for the event card.
/// </summary>
public EventPageViewModel? EventCardData { get; set; }
/// <summary>
/// true to show the organization popup, false to hide it.
/// </summary>
public bool OrgPopupVisible { get; set; }
/// <summary>
/// The header text for the organization popup.
/// </summary>
public string? OrgPopupHeader { get; set; }
/// <summary>
/// The data for the organization card.
/// </summary>
public OrganizationPageModel? OrgCardData { get; set; }
/// <summary>
/// The GetHashCode() of Data the last time we updated it in SetParametersAsync(). We use this to know
/// when Data has changed.
/// </summary>
private int _dataHashCode;
/// <summary>
/// The center of the map last time we called MapMoved. We use this to not call MapMoved if the map center
/// is moved less than 10 miles.
/// </summary>
private Position? _lastCenter;
/// <summary>
/// The bounds of the map last time we called MapMoved. We use this to not call MapMoved if the map is
/// zoomed in, or zoomed back out to a level <= the level of the last call to MapMoved.
/// </summary>
private BoundingBox? _lastBounds;
/// <inheritdoc />
protected override async Task OnInitializedAsync()
{
await base.OnInitializedAsync();
if (string.IsNullOrEmpty(ReturnUrl))
ReturnUrl = "/User/Search";
}
public async Task OnMapReadyAsync(MapEventArgs mapArgs)
{
try
{
if (!_firstMapReadyComplete)
{
// move it to the search center
if (MapCenter is not null)
{
await mapArgs.Map.SetCameraOptionsAsync(options =>
options.Center = new Position(MapCenter.X, MapCenter.Y));
// tell the parent (above call does not call the move event
var cameraOptions = await mapArgs.Map.GetCameraOptionsAsync();
await NewMapLocationAndExtent(cameraOptions.Center, cameraOptions.Bounds, cameraOptions.Zoom, true);
}
else
{
var currentPositionResult = await GeolocationService.GetCurrentPosition();
if (currentPositionResult.IsSuccess)
MapCenter = new TopologyPoint(currentPositionResult.Position.Coords.Longitude,
currentPositionResult.Position.Coords.Latitude)
{ SRID = 4326 };
else if (User.Address.Location is not null)
MapCenter = new TopologyPoint(User.Address.Location.X,
User.Address.Location.Y)
{ SRID = 4326 };
else
{
Trap.Break();
MapCenter = new TopologyPoint(-77.035278, 38.889484) { SRID = 4326 };
}
await mapArgs.Map.SetCameraOptionsAsync(options =>
options.Center = new Position(MapCenter.X, MapCenter.Y));
// we don't tell the parent because it didn't set it for us.
}
}
// create the datasources
EntitiesDataSource = new DataSource(EntitiesDataSourceId)
{
EventActivationFlags = DataSourceEventActivationFlags.None(),
Options = new()
{
Cluster = true,
ClusterRadius = 50,
ClusterMaxZoom = 24
}
};
await mapArgs.Map.AddSourceAsync(EntitiesDataSource);
// add the pins
if (Data is not null)
{
await AddEventPins(Data.EventData);
await AddOrganizationPins(Data.OrgData);
}
else Trap.Break(); // handled when set later?
// set up the popup. We have just one that is opened/updated/closed
PinTooltip = new Popup(new PopupOptions
{
// Position is set in OnPinMove()
Position = MapCenter is null ? new Position(0, 0) : new Position(MapCenter!.X, MapCenter.Y),
PixelOffset = new Pixel(0, -20),
CloseButton = true,
OpenOnAdd = false,
});
await mapArgs.Map.AddPopupAsync(PinTooltip);
}
catch (Exception ex)
{
LoggerEx.LogError(ex, "OnMapReadyAsync");
ex.Data.TryAdd("Category", typeof(SearchMap));
throw;
}
finally
{
_firstMapReadyComplete = true;
}
}
public async Task OnDatasourceAdded(MapSourceEventArgs mapArgs)
{
try
{
var singleItemLayer = new SymbolLayer
{
Options = new()
{
Source = mapArgs.Source.Id,
IconOptions = new IconOptions
{
Image = new ExpressionOrString("pin-blue")
},
Filter = new(new[]
{
new ExpressionOrString("!"),
new Expression(new []
{
new ExpressionOrString("has"),
new ExpressionOrString("point_count"),
})
})
},
EventActivationFlags = LayerEventActivationFlags.None()
.Enable(LayerEventType.Click)
.Enable(LayerEventType.MouseOver)
.Enable(LayerEventType.MouseOut)
};
await mapArgs.Map.AddLayerAsync(singleItemLayer);
}
catch (Exception ex)
{
LoggerEx.LogError(ex, "OnDatasourceAdded");
Trap.Break();
ex.Data.TryAdd("Category", typeof(SearchMap));
throw;
}
}
/// <inheritdoc />
public override async Task SetParametersAsync(ParameterView parameters)
{
if ((!_firstMapReadyComplete) || EntitiesDataSource is null)
{
await base.SetParametersAsync(parameters);
return;
}
parameters.SetParameterProperties(this);
try
{
foreach (var parameter in parameters)
{
if (parameter.Name != nameof(Data) || parameter.Value is not SearchMapPageModel paramData)
continue;
// if the data hasn't changed, then we're done
var valueHashCode = paramData.GetHashCode();
if (valueHashCode == _dataHashCode)
continue;
_dataHashCode = valueHashCode;
if (EntitiesDataSource.Shapes is not null && EntitiesDataSource.Shapes.Any())
await EntitiesDataSource!.ClearAsync();
List<SearchMapEntity> listEvents = paramData.EventData;
List<SearchMapEntity> listOrgs = paramData.OrgData;
// set the new/additional pins
await AddEventPins(listEvents);
await AddOrganizationPins(listOrgs);
break;
}
await base.SetParametersAsync(ParameterView.Empty);
}
catch (Exception ex)
{
LoggerEx.LogError(ex, "SetParametersAsync");
ex.Data.TryAdd("Category", typeof(SearchMap));
throw;
}
}
private async Task AddEventPins(List<SearchMapEntity> listEvents)
{
var listPins = new List<Shape>();
foreach (var entity in listEvents)
{
// if no address location, we can't pin it
var location = entity.Location;
if (location is null)
continue;
var props = new Dictionary<string, object>
{
{ "Type", _typeEvent },
{ "Id", entity.Id },
{ "UniqueId", entity.UniqueId },
{ "Name", entity.Name },
{ "ProfileUrl", entity.ProfileUrl},
{ "Description", entity.Description ?? string.Empty }
};
if (entity.RecurrenceInfoId is not null)
{
props.Add("RecurrenceInfoId", entity.RecurrenceInfoId!);
props.Add("RecurrenceIndex", entity.RecurrenceIndex!);
}
var pin = new Shape<AtlasMapPoint>(new AtlasMapPoint(
new Position(location.X, location.Y)), props);
listPins.Add(pin);
}
await EntitiesDataSource!.AddAsync(listPins);
}
private async Task AddOrganizationPins(List<SearchMapEntity> listOrganizations)
{
var listPins = new List<Shape>();
foreach (var entity in listOrganizations)
{
// if no address location, we can't pin it
var location = entity.Location;
if (location is null)
continue;
var props = new Dictionary<string, object>
{
{ "Type", _typeOrganization },
{ "Id", entity.Id },
{ "Name", entity.Name },
{ "ProfileUrl", entity.ProfileUrl},
{ "Description", entity.Description ?? string.Empty }
};
var pin = new Shape<AtlasMapPoint>(new AtlasMapPoint(
new Position(location.X, location.Y)), props);
listPins.Add(pin);
}
await EntitiesDataSource!.AddAsync(listPins);
}
private static int? ConvertToInt(object? number)
{
if (number is null)
return null;
if (number is JsonElement jsonElement)
return jsonElement.GetInt32();
return Convert.ToInt32(number);
}
private static string? ConvertToString(object? value)
{
if (value is null)
return null;
if (value is JsonElement jsonElement)
return jsonElement.GetString() ?? string.Empty;
return value.ToString() ?? string.Empty;
}
private async Task OnMapMoveEnd(MapEventArgs mapArgs)
{
try
{
if (!_firstMapReadyComplete)
return;
// we decide if we call the event based on the center & extent on the last call vs.
// the current center & extent as set in the CameraOptions.
var cameraOptions = await mapArgs.Map.GetCameraOptionsAsync();
// new extent outside the old one?
// allow a little shift on the edges
var extentChanged = _lastBounds is null ||
cameraOptions.Bounds.North > _lastBounds.North + LatitudeOneMile ||
cameraOptions.Bounds.South < _lastBounds.South - LatitudeOneMile ||
cameraOptions.Bounds.West < _lastBounds.West - LongitudeOneMile ||
cameraOptions.Bounds.East > _lastBounds.East + LongitudeOneMile;
bool centerChangedEnough;
if (_lastCenter is null)
centerChangedEnough = true;
else
{
// If under 5 miles on both axis, don't do anything. We grow the extent by 10 miles
// so this should be good.
var latitudeDiff = Math.Abs(_lastCenter.Latitude - cameraOptions.Center.Latitude);
var longitudeDiff = Math.Abs(_lastCenter.Longitude - cameraOptions.Center.Longitude);
centerChangedEnough = latitudeDiff > LatitudeOneMile * 5.0 || longitudeDiff > LongitudeOneMile * 5.0;
}
await NewMapLocationAndExtent(cameraOptions.Center, cameraOptions.Bounds, cameraOptions.Zoom,
extentChanged || centerChangedEnough);
}
catch (Exception ex)
{
LoggerEx.LogError(ex, "OnMapMoveEnd");
ex.Data.TryAdd("Category", typeof(SearchMap));
throw;
}
}
private async Task NewMapLocationAndExtent(Position centerPosition, BoundingBox box, double? zoom, bool runSearch)
{
// We expand the box here, both for _lastBounds and extentRectangle. This increases the search area
// and means the camera.Bounds will be inside _lastBounds for moves under 10 miles.
var north = box.North + LatitudeOneMile * 10.0;
var south = box.South - LatitudeOneMile * 10.0;
var west = box.West - LongitudeOneMile * 10.0;
var east = box.East + LongitudeOneMile * 10.0;
// these are what we sent calling the event. Used solely to determine if the next move
// needs to run a new search.
if (runSearch)
{
_lastCenter = new Position(centerPosition.Longitude, centerPosition.Latitude);
_lastBounds = new BoundingBox(west, south, east, north);
}
var centerPoint = new TopologyPoint(centerPosition.Longitude, centerPosition.Latitude)
{
// // WGS 84 coordinate system (per ChatGPT)
SRID = 4326
};
// Define the four corners of the rectangle
var upperLeft = new Coordinate(west, north);
var lowerLeft = new Coordinate(west, south);
var lowerRight = new Coordinate(east, south);
var upperRight = new Coordinate(east, north);
// Create a linear ring (a closed line string) from the corners
// Must be counterclockwise. If clockwise, that's the outside of the polygon.
var coordinates = new[] { upperLeft, lowerLeft, lowerRight, upperRight, upperLeft };
var extentRing = new LinearRing(coordinates)
{
SRID = 4326
};
// Create the polygon from the linear ring
var extentRectangle = new TopologyPolygon(extentRing)
{
SRID = 4326
};
if (MapMoved.HasDelegate)
await MapMoved.InvokeAsync((centerPoint, extentRectangle, zoom ?? 12, runSearch));
else
MapCenter = centerPoint;
}
}
}