ThinkGeo.com    |     Documentation    |     Premium Support

Migrating from v13 to v14

You can just add blankCircleStyle to the layer as a separate style (instead of part of the TextStyle).

If you are creating a style for cluster points, it’s better to use a ClusterPointStyle, where you can input an UnClusteredPointStyle, ClusteredPointStyle and a TextStyle, like following.

     var clusterPointStyle = new ClusterPointStyle(unclusteredPointStyle, textStyle)
     {
         MinimumFeaturesPerCellToCluster = 2,
         ClusteredPointStyle = clusteredPointStyle
     };

We have a sample in HowDoI, please check it out.

Thanks Ben, I took the first approach which is adding the point style as separate layer style.
I found that for the TextStyle, some characters are not showing in the latest version.
In below example, those should be ‘99+’ are not showing correctly.

Hi Jimmy, are you using “Segoe UI”? can you run the following piece of code, which writes “90+” to an image, and use the font you were using in your application, and let me know if the generated image looks good?

            SkiaGeoCanvas canvas = new SkiaGeoCanvas();
            var geoImage = new GeoImage(800, 800);
            canvas.BeginDrawing(geoImage, new RectangleShape(-1000, 1000, 1000, -1000), GeographyUnit.Meter);
            var geoFont = new GeoFont("Segoe UI", 120, DrawingFontStyles.Bold);
            canvas.DrawText("90+", geoFont,GeoBrushes.White, GeoPens.Black, new ScreenPointF[]{new ScreenPointF(400, 400)}, DrawingLevel.LabelLevel, 0, 0, DrawingTextAlignment.Center );
            canvas.EndDrawing();

            geoImage.Save("d:\\test\\result.png");

yes, and it just not working after updating the library.
I guess Segoe UI does not exist in android / iOS, after switch to sans-serif, the character is successfully generated in android (not tested in iOS yet).

However, it looks I cannot use my custom font, I have initialized my fonts but no one work in generating the image

builder.ConfigureFonts(fonts =>
{
fonts.AddFont(“OpenSans-Regular.ttf”, “OpenSansRegular”);
fonts.AddFont(“Bold.otf”, “BoldFontStyle”);
fonts.AddFont(“ExtraLight.otf”, “ExtraLightFontStyle”);
fonts.AddFont(“Light.otf”, “LightFontStyle”);
fonts.AddFont(“Medium.otf”, “MediumFontStyle”);
fonts.AddFont(“Regular.otf”, “RegularFontStyle”);
fonts.AddFont(“SemiBold.otf”, “SemiBoldFontStyle”);
});

Hi Jimmy,

Here’s why it behaves this way and how to fix it:

1. "Segoe UI" doesn’t exist on Android / iOS

When the requested font doesn’t exist, Skia falls back based on the first character .
In "90+" , it found NotoSansCJKsc-Regular , which handles 9 and 0 , but not + , so it showed a tofu block.

2. ConfigureFonts(...) only applies to MAUI UI controls

It does not apply to SkiaSharp (used by ThinkGeo for rendering). Skia needs a real font file, not the MAUI font alias. If the font files are under Resources/Fonts and marked as MauiFont , use something like:

string fontPath = await GetFontTempFilePathAsync("OpenSans-Regular.ttf");

var textStyle = new TextStyle(
    "Name",
    new GeoFont(fontPath, 12, DrawingFontStyles.Bold),
    GeoBrushes.DarkRed
);
public static async Task<string> GetFontTempFilePathAsync(string fileName)
{
    var cacheDir = FileSystem.CacheDirectory;
    var targetPath = Path.Combine(cacheDir, fileName);

    if (File.Exists(targetPath))
        return targetPath;

    Directory.CreateDirectory(cacheDir);

    using var src = await FileSystem.OpenAppPackageFileAsync(fileName);
    using var dest = File.Create(targetPath);
    await src.CopyToAsync(dest);

    return targetPath;
}

You raised a very valid point — the current font related API is a bit confusing and may behave differently across platforms. Let us think about it how to improve it. We will keep you posted.

Thanks,
Ben

Hi Jimmy,

Please pull the latest v14.5.0-beta039 and give it another try. In this version we use Open Sans as the fallback font instead of selecting the fallback based on the first character. We’ve also added a new event, TileOverlay.CreatingSKTypefaceForCharacter , which allows you to configure the typeface for a given character and font. A new sample will be added to HowDoI to demonstrate how to use this event.

Thanks,
Ben

Thank you for your prompt action Ben.
Unfortunately I am not able to find the latest sample project from ThinkGeo / Public / Mobile Maps · GitLab. Does the branch exist for public?

In addition to this, how do ThinkGeo support on different projection.
I was told to switch my map tile server from EPSG3857 to EPSG7899
Pervious it is straight forward to use EPSG3857 with World_Imagery (MapServer), but it seems there are some settings need to change

The new map tile configuration (private repo)

What I have done are

  1. Setup the Custom WebRasterXyzTileAsyncLayer with the ProjectionString and TileMatrixSet (MyMapLayer.cs)
  2. Set the CustomZoomLevelSet in the overlay

The problem I ran into are

  1. The GetImageUriAsyncCore method does not returning the correct X and Y value (start with 0). I have to go through a custom method to calculate. However, it seems the value does not correct on larger zoom level. (At least it looks like that, but it could be caused by the lat lon projection?)

For example, the red dot is located in wrong location, it suppose to be location of National Gallery of Victoria - Google Maps

I hope the attached project could help but the map server is private so probably the tile will not load from your side.
MauiApp1.zip (550.9 KB)

On the other hand, my web app is using below settings in Leaflet. Not sure if this is helpful for you.

EMAP_COORDINATE_REFERENCE_SYSTEM = new L.Proj.CRS(

'EPSG:7899',

`+proj=lcc +lat_1=-36 +lat_2=-38 +lat_0=-37 +lon_0=145 +x_0=2500000

+y_0=2500000 + ellps=GRS80 + towgs84=0, 0, 0, 0, 0, 0, 0 + units=m + no_defs `,

{

  origin: [-38675897, 62145254],

  resolutions: [

    2116.670900008467, 1058.3354500042335, 529.1677250021168, 264.5838625010584, 132.2919312505292, 66.1459656252646, 46.30217593768521,

    26.458386250105836, 13.229193125052918, 6.614596562526459, 2.6458386250105836, 1.3229193125052918, 0.6614596562526459

  ]

}

);

Jimmy,

We didn’t have a chance to create a sample for it. We do have a sample for desktop though, it would be similar: https://gitlab.com/thinkgeo/public/thinkgeo-desktop-maps/-/blob/develop/samples/wpf/HowDoISample/Samples/MapOnlineData/MVTWithLocalFonts.xaml.cs?ref_type=heads

About the Projection:
It’s because we need to apply the original point to VicGridFullExtent, so instead of:

TileMatrixSet.CreateTileMatrixSet(
    MapConfig.TileSize,
    MapConfig.VicGridFullExtent,   // ❌ wrong extent 
    GeographyUnit.Meter,
    MapConfig.MapZoomingDetails.Select(p => p.Scale));

let’s do:

public static class MapConfig
{
    public static RectangleShape CreateTileWorldExtent()
    {
        var level0 = MapZoomingDetails.Single(z => z.ZoomLevel == 0);
        double res0 = level0.Resolution;
        double tileSizeMeters = TileSize * res0;

        var full = VicGridFullExtent;

        var cols0 = Math.Ceiling((full.UpperRightPoint.X - OriginX) / tileSizeMeters);
        var rows0 = Math.Ceiling((OriginY - full.LowerLeftPoint.Y) / tileSizeMeters);

        double worldLeft   = OriginX;
        double worldTop    = OriginY;
        double worldRight  = worldLeft + cols0 * tileSizeMeters;
        double worldBottom = worldTop  - rows0 * tileSizeMeters;

        return new RectangleShape(worldLeft, worldTop, worldRight, worldBottom);
    }
}}

and we don’t need to apply that Original in GetImageUriAsyncCore, do the following instead:

public class MyMapLayer : WebRasterXyzTileAsyncLayer
{
    private const string BaseUrl =
        "https://mymapserver.com/arcgis/rest/services/mapscape_gda2020/MapServer";
    private const string Token = "tokenHere";

    public MyMapLayer()
        : base(MapConfig.TileSize, GeographyUnit.Meter, MapConfig.VicGridFullExtent)
    {
        Projection = new Projection(MapConfig.ProjString);

        var worldExtent = MapConfig.CreateTileWorldExtent();

        TileMatrixSet = TileMatrixSet.CreateTileMatrixSet(
            MapConfig.TileSize,
            worldExtent,                  // use this new origin-based world extent
            GeographyUnit.Meter,
            MapConfig.MapZoomingDetails.OrderBy(z => z.ZoomLevel).Select(z => z.Scale));
    }

    protected override Task<string> GetImageUriAsyncCore(
        int zoomLevel, long x, long y, float resolutionFactor)
    {
        string url = $"{BaseUrl}/tile/{zoomLevel}/{y}/{x}?token={Token}";
        return Task.FromResult(url);
    }
}

Let me know if you still see any issues.

Thanks,
Ben

Thanks Ben,

I have tried to apply the world extent and it seems the tile is downloading now.
But it looks there is still issue on the lat lon / meter conversion or tile shifted?
I have set a location -37.828667,144.9794901 in google map and thinkGeo map, but they are pinning in different location. Any idea?

Google map
googlemap
ThinkGEO

Hi Jimmy,

Here is what I see on Google Map, can you check it out why it’s different than yours?

37°49’43.2"S 144°58’46.2"E - Google Maps

Also, can you make sure you’re using the same coordinates in the ThinkGeo map and send me a new screenshot if it’s different?

If the issue still exists, could you also render your EPSG:7899 service and the same point in QGIS as well? Some servers have the world slightly off, and I’d like to check if that’s what’s happening here.

Thanks,
Ben

Hi Ben,

Here is the results:
Google map
37°49’43.2"S 144°58’46.2"E - Google Maps
ThinkGEO

QGIS (I am not quite good at this software, but the point is located at the intersection)

so it looks the point in ThinkGEO is different?

Thank you

Hi Jimmy,

We verified the point was projected correctly to EPSG7899, so it should because of the shifting of the wmts tiles. Can you create a sub class inheriting from WmtsOverlay, and override the following method:

    protected override TileView GetTileCore()
    {
	    var tile = base.GetTileCore();
        tile.WidthRequest *= MapUtil.StandardDpi / MapUtil.WmtsStandardDpi;
        tile.HeightRequest *= MapUtil.StandardDpi / MapUtil.WmtsStandardDpi;
        return tile;
    }

Let me know what the map looks like now.

Thanks,
Ben

Hi Ben, since I am using LayerOverlay instead of WmtsOverlay, I have tried the suggestion in my LayerOverlay but the location is still wrong. Attached the latest sample code and the arcgis server config json


MauiApp1.zip (940.5 KB)

Before you switched to the new EPSG:7899 projection, did everything work correctly, did the point appear in the expected location?

Also, is there any chance we could get temporary access to this 7899 server? That would be very helpful. If so, please send the connection details to support@thinkgeo.com.

Hi Ben,

It was working fine with 3857 https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{zoomLevel}/{y}/{x} which is a public service.
Regarding to the connection, I can try to ask for it.

Hi Ben,

I found that we have another map server that use the same Spatial Reference: 102171 (3111)
Have sent to the email provided above.
By the way, I am trying to converting some map UI from Ersi to thinkGEO.
Any idea on how could I convert below code snippet?

   private async Task<FeatureLayer> CreateSymbolizedFeatureLayer(MapLayer mapLayer)
   {
       if (mapLayer == null)
       {
           throw new LoadMapLayerFailedException("Map layer failed to load. Parameter mapLayer must not be null.");
       }

       FeatureLayer featureLayer;
       try
       {
           // define an online data source
           string baseUri = string.Empty;
           if (mapLayer.Type == LayerType.CLReserve || mapLayer.Type == LayerType.CLTenure || mapLayer.Type == LayerType.PublicReservePark)
           {
               baseUri = MapshareConstants.GIS_MAPSHARE_BASE_URL;
           }
           else
           {
               baseUri = MapLayerConstants.OCR_INTEL_MAPSERVER_BASE;
           }
// E.g. Uri will be https://maps.ffm.vic.gov.au/arcgis/rest/services/ocr_intel_services/MapServer/0
           var uri = new Uri(baseUri + mapLayer.FeatureUrl);
           var featureTable = new ServiceFeatureTable(uri);

           // await loading of the table then check the load status
           await featureTable.LoadAsync();

           if (featureTable.LoadStatus == LoadStatus.Loaded)
           {
               // create a new feature layer, pass the service feature table to the constructor
               featureLayer = new FeatureLayer(featureTable);

               featureLayer.FeatureTable.Layer.Opacity = _layerOpacity;

               SimpleLineSymbol lineSymbol = new SimpleLineSymbol(SimpleLineSymbolStyle.Solid, mapLayer.OutlineColor, 4);
               SimpleFillSymbol fillSymbol = new SimpleFillSymbol(SimpleFillSymbolStyle.Solid, mapLayer.FillColor, lineSymbol);

               featureLayer.Renderer = new SimpleRenderer(fillSymbol);

               return featureLayer;
           }

           return null;
       }
       catch (Exception ex)
       {
           _errorLogger.LogException(ex);
           Debug.WriteLine(ex.Message);
           return null;
       }
   }

I am able to use ArcGisServerRestAsyncLayer to do similar stuff but I am not sure how to do exactly the same in the ersi about the render style

        var arcGisLayer = new ArcGisServerRestAsyncLayer(new Uri("https://maps.ffm.vic.gov.au/arcgis/rest/services/ocr_intel_services/MapServer/export"));

        // Sets our parameters for image format.
        arcGisLayer.ImageFormat = ArcGISServerRestLayerImageFormat.Png; // we can use the property 'ImageFormat' instead of the 'format' in parameters. When they coexist, the property is perferred.
        // Sets our parameters for transparency.
        arcGisLayer.Parameters.Add("transparent", "true");
        // Specifies the layerId(s) you wish to display.  LayerId 2 is the county layer.
        arcGisLayer.Parameters.Add("layers", "show:9");

        overlay.Layers.Add(arcGisLayer);

Hi Jimmy,

We tested your 102171 server you sent through email, and it worked as expected. The same point (-37.828667,144.9794901) displayed at the same spot (red dot) as on Google Map. I’ve sent you the code through email

We will keep working on the other questions in your post.

Thanks,
Ben

Thanks Ben, that is a good progress.
Unfortunately I cannot use WmtsAsyncLayer directly as the app would to support offline.
Any chance we could implement something in MyLayer.cs to achieve the same result? it looks different for my case…
MauiApp1.zip (1.0 MB)

Hi Jimmy,

  1. I see the issues in MyMapLayer, which inherits from WebRasterXyzTileAsyncLayer. I also see MyWmtsLayer : WmtsAsyncLayer, which works as expected and plot the point in the right spot.

  2. How about use your MyWmtsLayer instead of MyMapLayer? Because a: inheriting from WebRasterXyzTileAsyncLayer means you need to rewrite wmts in your subclass, that’s lot of work. it would be much easier to just override WmtsAsyncLayer; b: WmtsAsyncLayer also inherits from WebRasterXyzTileAsyncLayer, which means you still have lots of feasibility in your sub class.

  3. I’m a bit confused about the “offline support”. If you meant an offline wmts server, you can just use WmtsAsyncLayer, or its subclass (you current MyWmtsLayer) to hit that server; If you mean the cached image on the client side, you can implement it by using TileCache or overriding FetchImageAsyncCore.

  4. I see some issues in your current code, for example used 8000000d as the base scale instead of 7559538.928600717d which is defined in the server, but it should not be an issue if inheriting from WmtsAsyncLayer which will grab the correct scale set automatically.

I hope that makes sense. Let us know if you have any questions.

Thanks,
Ben

Hi Ben,

For the offline support, our app is designed to be able to run under flight mode (I.E. out of Internet access). We have currently support below features

  1. Allow user to download a specified area tiles
  2. Manually toggle to load tiles from server / local
    Currently, we override this method to make it happen
        protected override async Task<RasterTile> GetTileAsyncCore(int zoomLevel, long x, long y, float resolutionFactor, CancellationToken cancellationToken)
        {
            CurrentZoom = zoomLevel;
            MapTile mapTile = new MapTile { ZoomLevel = zoomLevel, X = x, Y = y };
            bool tileCached = MapTileService.IsTileCached(mapTile);
            byte[] tileByteArray = null;
            if (ShowCached)
            {
                // If user prefer showing cached tiles, we will not downloadd the tile from the web service even it is not cached. We show blank tile instead.
                if (tileCached)
                    // If the tile is cached, read it from the cache
                    tileByteArray = File.ReadAllBytes(MapTileService.GetCachedTilesPath(mapTile));
                else
                    // If the tile is not cached, show a blank tile
                    tileByteArray = MapTileService.GetBlankTile();
            }
            else
            {
                // If user does not prefer showing cached tiles, we will try to download the tile from the web service if there is no cached tile.
                if (tileCached)
                    tileByteArray = File.ReadAllBytes(MapTileService.GetCachedTilesPath(mapTile));
                else
                {
                    if (!_networkStatusService.OperateUnderOfflineMode)
                    {
                        try
                        {
                            // Try to download the tile from the web service
                            await MapTileService.CacheMapTile(mapTile);
                            tileByteArray = await Task.Run(() => { return File.ReadAllBytes(MapTileService.GetCachedTilesPath(mapTile)); });
                        }
                        catch (Exception ex)
                        {
                            // Show a blank tile if there is an error
                            tileByteArray = MapTileService.GetBlankTile();
                        }
                    }
                    else
                    {
                        // If we are under offline mode, we will not try to download the tile from the web service.
                        tileByteArray = MapTileService.GetBlankTile();
                    }

                }
            }
            return new RasterTile(tileByteArray, zoomLevel, x, y);
        }

I am open to use WmtsAysncLayer but I am not sure what should I do if the device is out of network.
It seems that if I am out of network, the layer is not loaded at all because it cannot load the settings from WMTS, and there is another issue that how can I do if the map service does not support WMTS?
image
It looks one of our map server does not support it…