wfletcher / EasyGIS.NET

EasyGIS.NET source
GNU Lesser General Public License v3.0
68 stars 24 forks source link

Tile XYZ to WMS #94

Open theo2f opened 2 years ago

theo2f commented 2 years ago

Hi! How can I convert the XYZ Tile format to WMS Tile format? I see many maps on web using this format and I would like to use them.

URL sample: https://ies-ows.jrc.ec.europa.eu/gwis?service=WMS&request=GetMap&layers=admin.countries_borders&styles=&format=image%2Fpng&transparent=true&version=1.1.1&singletile=false&width=2048&height=2048&srs=EPSG%3A3857&bbox=0,5009377.085697314,2504688.5428486555,7514065.628545967

I did some things, like:

Tile URL

Since I'm using the TileCollection class from EGIS sample (with some mods) I changed the parameter format to check and replace later:

https://ies-ows.jrc.ec.europa.eu/gwis?service=WMS&request=GetMap&layers=admin.countries_borders&styles=&format=image%2Fpng&transparent=true&version=1.1.1&singletile=false&width=256&height=256&srs=EPSG%3A3857&bbox={bbLeft},{bbBottom},{bbRight},{bbTop}

Extend converter

This class provide the conversion from XYZ to a extended long/lat. I tryed to use the TileUtil.GetTileLatLonBounds method but I could't make it work:


public static class xnGeoUtilsLongLat
{
public class Extend
{
public double Left { get; set; }
public double Right { get; set; }
public double Bottom { get; set; }
public double Top { get; set; }
public double Res { get; set; }
    public Extend()
    { }
}
public class Tile
{
    public double X { get; set; }
    public double Y { get; set; }
    public int Z { get; set; }
    public double Top { get; set; }
    public double Left { get; set; }

    public Tile()
    { }
}

// web mercator projection extent
static Extend ProjExtent = new Extend
{
    Left = -20037508.342789244,
    Right = 20037508.342789244,
    Bottom = -20037508.342789244,
    Top = 20037508.342789244
};
//tile seize
static int TileSize = 256;

// resolutions
static double[] Resolutions = new double[] {
        156543.03392804097, 78271.51696402048, 39135.75848201024,
        19567.87924100512, 9783.93962050256, 4891.96981025128, 2445.98490512564,
        1222.99245256282, 611.49622628141, 305.748113140705, 152.8740565703525,
        76.43702828517625, 38.21851414258813, 19.109257071294063, 9.554628535647032,
        4.777314267823516, 2.388657133911758, 1.194328566955879, 0.5971642834779395,
        0.29858214173896974, 0.14929107086948487, 0.07464553543474244,
        0.03732276771737122, 0.01866138385868561 };

static List<Tile> GetTiles(Extend extent, int z)
{
    //coordinated in pixel
    var lx = Math.Floor((extent.Left - ProjExtent.Left) / Resolutions[z]);
    var rx = Math.Floor((extent.Right - ProjExtent.Left) / Resolutions[z]);
    var by = Math.Floor((ProjExtent.Top - extent.Bottom) / Resolutions[z]);
    var ty = Math.Floor((ProjExtent.Top - extent.Top) / Resolutions[z]);

    // tile numbers
    var lX = Math.Floor(lx / TileSize);
    var rX = Math.Floor(rx / TileSize);
    var bY = Math.Floor(by / TileSize);
    var tY = Math.Floor(ty / TileSize);

    //top left tile position of top-left tile with respect to window/div
    var top = (tY * TileSize) - ty;
    var topStart = top;
    var left = (lX * TileSize) - lx;
    var tiles = new List<Tile>();
    for (var i = lX; i <= rX; i++)
    {
        top = topStart;
        for (var j = tY; j <= bY; j++)
        {
            tiles.Add(new Tile
            {
                X = i,
                Y = j,
                Z = z,
                Top = top,
                Left = left
            });
            top += TileSize;
        }
        left += TileSize;
    }
    return tiles;
}

public static Extend TileExtent(double x, double y, int z)
{
    return TileExtent(new xnGeoUtilsLongLat.Tile()
    {
        X = x,
        Y = y,
        Z = z
    });
}
public static Extend TileExtent(Tile tile)
{
    var right = ProjExtent.Left + tile.X * TileSize * Resolutions[tile.Z];
    var left = right - TileSize * Resolutions[tile.Z];
    var bottom = ProjExtent.Top - tile.Y * TileSize * Resolutions[tile.Z];
    var top = bottom + TileSize * Resolutions[tile.Z];

    return new Extend()
    {
        Left = left,
        Right = right,
        Bottom = bottom,
        Top = top,
        Res = Resolutions[tile.Z]
    };
}

}


**CreateBitmap code**
> Here the CreateBitmap method that was changed to check if imageUrlFormat contains "{bbLeft}".
> Case yes then use WMS format else keep using XYZ.
```C#
private System.Drawing.Bitmap CreateBitmap()
{
    try
    {
        string strUrl = "";
        if (imageUrlFormat.Contains("{bbLeft}"))
        {
            //force dot as decimal separator
            NumberFormatInfo nfi = new NumberFormatInfo();
            nfi.NumberDecimalSeparator = ".";

            var _ext = xnGeoUtilsLongLat.TileExtent(this.x, this.y, this.zoomLevel);

            imageUrlFormat = imageUrlFormat
                .Replace("{bbLeft}", "{0}")
                .Replace("{bbBottom}", "{1}")
                .Replace("{bbRight}", "{2}")
                .Replace("{bbTop}", "{3}");

            strUrl = string.Format(imageUrlFormat, _ext.Left.ToString(nfi), _ext.Bottom.ToString(nfi), _ext.Right.ToString(nfi), _ext.Top.ToString(nfi));
        }
        else
            strUrl = string.Format(imageUrlFormat, this.zoomLevel, this.x, this.y);

        Console.WriteLine(strUrl);

        var bitmapStream = GetFromCache(strUrl);
        if (bitmapStream != null)
        {
            bitmapStream.Seek(0, System.IO.SeekOrigin.Begin);
            using (Image img = Bitmap.FromStream(bitmapStream))
            {
                //copy the returned image to a new bitmap. If we don't do this then the transparency may not
                //display properly
                Bitmap bm = new Bitmap(img.Width, img.Height);
                using (Graphics g = Graphics.FromImage(bm))
                {
                    g.DrawImage(img, new Rectangle(0, 0, img.Width, img.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel);
                }
                return bm;
            }
        }

        //request not in local cache - retrieve asyncronously
        //dont await this call!
        GetBitmapAsync(strUrl);

        Bitmap blankBitmap = new Bitmap(256, 256);
        using (Graphics g = Graphics.FromImage(blankBitmap))
        {
            g.Clear(Color.LightGray);
        }
        return blankBitmap;
    }
    catch (Exception ex)
    {
        Bitmap blankBitmap = new Bitmap(256, 256);
        using (Graphics g = Graphics.FromImage(blankBitmap))
        {
            g.Clear(Color.LightGray);

        }
        return blankBitmap;
    }
}

It's working in part. Tiles is loaded, but it's not on the right position.

image

At moment I'm stuck on this and I can't go ahead. Can someone help me with this?

wfletcher commented 2 years ago

It looks like you are on the right track. This is the main code that you need.

static RectangleD GetWebMercatorTileLatLonBounds(int tileX, int tileY, int zoomLevel, int tileSize = 256)
        {
            if (zoomLevel < 0) throw new System.ArgumentException("zoomLevel must be >=0", nameof(zoomLevel));
            PointD topLeft = TileUtil.PixelToWebMercator((tileX * tileSize), (tileY * tileSize), zoomLevel, tileSize);
            PointD bottomRight = TileUtil.PixelToWebMercator(((tileX + 1) * tileSize), ((tileY + 1) * tileSize), zoomLevel, tileSize);
            return RectangleD.FromLTRB(topLeft.X, bottomRight.Y, bottomRight.X, topLeft.Y);
        }

        protected override string FormatTileUrl()
        {

            //https://www.e-education.psu.edu/geog585/node/699
            //bounding box specified by bottom left, top right coords
            //create bounding box
            var bounds = GetWebMercatorTileLatLonBounds(this.x, this.y, this.zoomLevel, 256);
            //0,5009377.085697314,2504688.5428486555,7514065.628545967
            string boundingBox = string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0:0.00000000},{1:0.00000000},{2:0.00000000},{3:0.00000000}",
                bounds.Left, bounds.Top, bounds.Right, bounds.Bottom);
            string strUrl = string.Format(System.Globalization.CultureInfo.InvariantCulture, imageUrlFormat, boundingBox);
            return strUrl;
        }

I created a branch named WMS-Tile-Server-Test with some modifications to test this. I'm not going to push this into the main branch (yet) as it needs some testing and the example WMS you supplied seems very slow and is hard to test. If you're patient it does seems to download all the tiles eventually though.

If you pull down this test branch you can see the mods to get it working

theo2f commented 2 years ago

Thank you very much, Winston! Working very well! I'm looking forward to seeing this in the main branch. I suggest you create a usage example, to make it easier for everyone.

About the performance on server that I sent the example, maybe you can use the URL below to make tests. Seems faster. https://ahocevar.com/geoserver/wms?SERVICE=WMS&VERSION=1.3.0&REQUEST=GetMap&FORMAT=image%2Fpng&TRANSPARENT=true&LAYERS=topp%3Astates&TILED=true&WIDTH=256&HEIGHT=256&CRS=EPSG%3A3857&STYLES=&BBOX=-13775786.985667605%2C5009377.085697312%2C-13149614.849955441%2C5635549.221409475