Skip to content
Snippets Groups Projects
VectorTile.cs 15.1 KiB
Newer Older
using Mapbox.Vector.Tile;
using System.Collections.Generic;
using System.IO;
Knuiman, Bart's avatar
Knuiman, Bart committed
using System.Linq;
using System.Threading.Tasks;
Knuiman, Bart's avatar
Knuiman, Bart committed
using UnityEditor;
Knuiman, Bart's avatar
Knuiman, Bart committed
using UnityEngine.Networking;
Knuiman, Bart's avatar
Knuiman, Bart committed
using static Wander.Easing;
Knuiman, Bart's avatar
Knuiman, Bart committed

namespace Wander
{
    // Always multiple of 3. Each 3 form a triangle.
Knuiman, Bart's avatar
Knuiman, Bart committed
    public class TriangulatedPolygon
Knuiman, Bart's avatar
Knuiman, Bart committed
        public List<Vector2> mins;
        public List<Vector2> maxs;
        public List<float>   denoms;

        public void OptimizeForIsPointInTriangle()
        {
            if ( vertices == null ) return; // Non poly layers have this.

            mins   = new List<Vector2>( vertices.Count / 3 );
            maxs   = new List<Vector2>( vertices.Count / 3 );
            denoms = new List<float>( vertices.Count / 3 );

            for ( int i = 0; i < vertices.Count; i += 3 )
            {
                float minx = float.MaxValue;
                float miny = float.MaxValue;    
                if (vertices[i+0].x < minx) minx = vertices[i].x;
                if (vertices[i+0].y < miny) miny = vertices[i].y;
                if (vertices[i+1].x < minx) minx = vertices[i+1].x;
                if (vertices[i+1].y < miny) miny = vertices[i+1].y;
                if (vertices[i+2].x < minx) minx = vertices[i+2].x;
                if (vertices[i+2].y < miny) miny = vertices[i+2].y;
                mins.Add( new Vector2( minx, miny ) );

                float maxx = float.MinValue;
                float maxy = float.MinValue;
                if (vertices[i+0].x > maxx) maxx = vertices[i].x;
                if (vertices[i+0].y > maxy) maxy = vertices[i].y;
                if (vertices[i+1].x > maxx) maxx = vertices[i+1].x;
                if (vertices[i+1].y > maxy) maxy = vertices[i+1].y;
                if (vertices[i+2].x > maxx) maxx = vertices[i+2].x;
                if (vertices[i+2].y > maxy) maxy = vertices[i+2].y;
                maxs.Add( new Vector2( maxx, maxy ) );

                // float denominator = ((vertex2.y - vertex3.y) * (vertex1.x - vertex3.x) + (vertex3.x - vertex2.x) * (vertex1.y - vertex3.y));
                float denominator = ((vertices[i+1].y - vertices[i+2].y) * (vertices[i].x - vertices[i+2].x) + (vertices[i+2].x - vertices[i+1].x) * (vertices[i].y - vertices[i+2].y));
                denoms.Add( 1.0f / denominator );
            }
        }
Knuiman, Bart's avatar
Knuiman, Bart committed
    public class VectorTile
    {
Knuiman, Bart's avatar
Knuiman, Bart committed
        public bool DownloadStarted => started;
        public bool Valid => valid;
        public bool Finished => finished;
        public bool Triangulated => triangulated;
Knuiman, Bart's avatar
Knuiman, Bart committed
        private bool started;
        private bool valid;
        private bool finished;
        private bool triangulated;
        private Task parseTileTask;
        private List<VectorTileLayer> layers;
        private List<List<TriangulatedPolygon>> polygonLayers;
Knuiman, Bart's avatar
Knuiman, Bart committed
        internal UnityWebRequest request;

        public void StartDownload()
        {
            Debug.Assert(!started);
            request.SendWebRequest();
            started = true;
        }
        {
            if (finished)
                return true;

            if (!request.isDone)
                return false;

Knuiman, Bart's avatar
Knuiman, Bart committed
            if (request.result == UnityWebRequest.Result.Success)
            {
                if (parseTileTask == null)
                {
Knuiman, Bart's avatar
Knuiman, Bart committed
                    var data = request.downloadHandler.data; // call on mainthread.
                    parseTileTask = Task.Run( () =>
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        var stream = new MemoryStream( data );
                        layers = VectorTileParser.Parse( stream );
                    } );
                }
                else if (parseTileTask.IsCompleted)
                {
                    valid = parseTileTask.IsCompletedSuccessfully;
                    finished = true;
                    parseTileTask = null;
                }
            }

            return finished;
        }

        // Number of failed polys to triangulate are returned.
        public int Triangulate()
            if (triangulated )
                return 0;
            int numFailedPolys = 0;
            polygonLayers = new List<List<TriangulatedPolygon>>();
            List<double> vertices = new List<double>();
            List<int> holeIndices = new List<int>();
            for ( int i =0; i < layers.Count; i++ )
            {
                var features = layers[i].VectorTileFeatures;
                var polygons = new List<TriangulatedPolygon>();
                for (int j = 0; j< features.Count; j++)
                {
Knuiman, Bart's avatar
Knuiman, Bart committed
                    polygons.Add( new TriangulatedPolygon() ); // Add empty to ensure list(layer) of lists(features) match.
                    if (features[j].GeometryType != Tile.GeomType.Polygon)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    {
                        continue;
Knuiman, Bart's avatar
Knuiman, Bart committed
                    }
                    vertices.Clear();
                    holeIndices.Clear();
                    var rings = features[j].Geometry;
                    for (int k = 0;k < rings.Count;k++)
                    {
                        if (k != 0) // if is not first ring, this is a hole
                        {
                            holeIndices.Add( vertices.Count / 2 );
                        }
                        var ring = rings[k];
                        for (int q = 0;q < ring.Count;q++)
                        {
                            vertices.Add( ring[q].X );
                            vertices.Add( ring[q].Y );
                        }
                    }
                    try
                    {
                        var indices = EarcutNet.Earcut.Tessellate( vertices, holeIndices );
                        TriangulatedPolygon poly = new TriangulatedPolygon();
                        poly.vertices = new List<Vector2>( indices.Count );
Knuiman, Bart's avatar
Knuiman, Bart committed
                        for (int k = 0;k < indices.Count; k++)
                        {
                            double x = vertices[indices[k]*2];
                            double y = vertices[indices[k]*2+1];
                            poly.vertices.Add( new Vector2( (float)x, (float)y ) );
                        }
Knuiman, Bart's avatar
Knuiman, Bart committed
                        polygons[polygons.Count-1] = poly;
                        Debug.Assert( poly.vertices.Count % 3 == 0 );
                    }
                    catch (Exception)
                    {
                        numFailedPolys++;
                    }
                }
                polygonLayers.Add( polygons );
            }
            triangulated = true;
            return numFailedPolys;
        }

Knuiman, Bart's avatar
Knuiman, Bart committed
        public void OptimizeForPointIsInsideTriangle()
        {
            for (int i = 0;i < polygonLayers.Count;i++)
                for (int j = 0;j < polygonLayers[i].Count;j++)
                    polygonLayers[i][j].OptimizeForIsPointInTriangle();
        }

Knuiman, Bart's avatar
Knuiman, Bart committed
        // Calls callback(x, y, channel) for each raster position (256 channels) where each value represents a single channel.
        // If no polygon was hit, 255 is called.
        // If polygon was hit, but no feature was matched, 254 is called.
Knuiman, Bart's avatar
Knuiman, Bart committed
        // Returns a list of failed to match pixels. This can be due to geometry not exactly matching or a layer not being found.
        public List<Vector3Int> RenderToTextureSingle(
            int width, int height, 
            List<string> matchingAttribKeys, 
            List<List<string>> layerNamesList, 
            Func<int, int, byte, bool> callback )
        {
            Debug.Assert( triangulated, "First call Triangulate." );
Knuiman, Bart's avatar
Knuiman, Bart committed
            Debug.Assert( layerNamesList.Count < 254, "254 and 255 are reserved for no hit or no match." );

            // First match layers to layer names.
Knuiman, Bart's avatar
Knuiman, Bart committed
            bool cancel = false;
            List<Vector3Int> failedPixels = new List<Vector3Int>();
Knuiman, Bart's avatar
Knuiman, Bart committed
            for (int l = 0;l < layers.Count && !cancel;l++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                var layer  = layers[l];
Knuiman, Bart's avatar
Knuiman, Bart committed
                for (int f = 0;f < layer.VectorTileFeatures.Count && !cancel;f++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                    var feature = layer.VectorTileFeatures[f];
                    feature.SelectedLayerIdx = 254;

                    if (feature.GeometryType != Tile.GeomType.Polygon)
                        continue;

                    if (feature.Attributes == null)
                        continue;

                    // Find matching layer.
                    bool matchingLayerFound = false;
                    for (int a = 0;a < feature.Attributes.Count && !matchingLayerFound;a++)
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        for (int m = 0;m < matchingAttribKeys.Count && !matchingLayerFound;m++)
                        {
                            if (feature.Attributes[a].Key != matchingAttribKeys[m])
                                continue;

                            string function = feature.Attributes[a].Value as string;
                            for (int layerIdx = 0;layerIdx < layerNamesList.Count && !matchingLayerFound;layerIdx++)
                            {
                                for (int layerNameIdx = 0;layerNameIdx < layerNamesList[layerIdx].Count;layerNameIdx++)
                                {
                                    if (function.Contains( layerNamesList[layerIdx][layerNameIdx] ))
                                    {
                                        feature.SelectedLayerIdx = layerIdx;
                                        matchingLayerFound = true;
                                        break;
                                    }
                                }
                            }
                        }
                    }

                    // try Layer.name
                    if (!matchingLayerFound)
                    {
Knuiman, Bart's avatar
Knuiman, Bart committed
                        for (int layerIdx = 0;layerIdx < layerNamesList.Count && !matchingLayerFound;layerIdx++)
                        {
                            for (int layerNameIdx = 0;layerNameIdx < layerNamesList[layerIdx].Count;layerNameIdx++)
                            {
Knuiman, Bart's avatar
Knuiman, Bart committed
                                if (layer.Name.Contains( layerNamesList[layerIdx][layerNameIdx] ))
Knuiman, Bart's avatar
Knuiman, Bart committed
                                {
                                    feature.SelectedLayerIdx = layerIdx;
                                    matchingLayerFound = true;
                                    break;
                                }
                            }
                        }
                    }
                }
            }

Knuiman, Bart's avatar
Knuiman, Bart committed
            Debug.Assert( width == height, "Must be square images." );

Knuiman, Bart's avatar
Knuiman, Bart committed
            // For each layer, for each pixel, now check triangle intersections.
            for (int l = 0;l < layers.Count && !cancel;l++)
            {
                var layer = layers[l];
Knuiman, Bart's avatar
Knuiman, Bart committed
                float fx  = (float)layer.Extent / width;
                TriangulatedPolygon cachedPoly = default;
                int cachedVtxIdx  = -1;
                int cachedTriIdx  = -1;
                byte cachedPixel  = 0;
Knuiman, Bart's avatar
Knuiman, Bart committed
                for (int y = 0;y < height && !cancel;y++)
                {
                    for (int x = 0;x < width && !cancel;x++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                        Vector2 p = new Vector2(fx*x+0.5f*fx, fx*y+0.5f*fx);
                        bool hit  = false;
Knuiman, Bart's avatar
Knuiman, Bart committed

Knuiman, Bart's avatar
Knuiman, Bart committed
                        // First try cache, quite often adjacent pixels will hit the same triangle.
                        if (cachedVtxIdx!=-1)
                        {
                            var vertices = cachedPoly.vertices;
                            var denoms   = cachedPoly.denoms;
                            var mins     = cachedPoly.mins;
                            var maxs     = cachedPoly.maxs;
                            if (!(p.x < mins[cachedTriIdx].x || p.x > maxs[cachedTriIdx].x) &&
                                !(p.y < mins[cachedTriIdx].y || p.y > maxs[cachedTriIdx].y))
                            {
                                hit = GeomUtil.PointIsInsideTriangle2( p, vertices[cachedVtxIdx], vertices[cachedVtxIdx+1], vertices[cachedVtxIdx+2], denoms[cachedTriIdx] );
                                if (hit)
                                {
                                    cancel = callback( x, y, cachedPixel ); // If SelectedLayerIdx did not match, it was set to 254.
                                    continue; // Pass from cache, continue to next pixel.
                                }
                            }
                        }

                        // Must check all triangles, no cache or was no hit with cache.
                        for (int f = 0;f < layer.VectorTileFeatures.Count;f++)
                        {
                            var feature = layer.VectorTileFeatures[f];

Knuiman, Bart's avatar
Knuiman, Bart committed
                            if (feature.GeometryType != Tile.GeomType.Polygon)
Knuiman, Bart's avatar
Knuiman, Bart committed
                            var polygons = polygonLayers[l][f];
                            if (polygons.vertices.Count == 0)
                                continue;

                            cachedVtxIdx = -1;
                            cachedPoly   = polygonLayers[l][f];
                            var mins     = cachedPoly.mins;
                            var maxs     = cachedPoly.maxs; 
                            var denoms   = cachedPoly.denoms;
                            var vertices = cachedPoly.vertices;
                            for (int vIdx = 0, triIdx = 0; vIdx < vertices.Count; vIdx += 3, triIdx++)
Knuiman, Bart's avatar
Knuiman, Bart committed
                                if ( p.x < mins[triIdx].x || p.x > maxs[triIdx].x ) continue;
                                if ( p.y < mins[triIdx].y || p.y > maxs[triIdx].y ) continue;
                                hit = GeomUtil.PointIsInsideTriangle2( p, vertices[vIdx], vertices[vIdx+1], vertices[vIdx+2], denoms[triIdx] );
Knuiman, Bart's avatar
Knuiman, Bart committed
                                if (hit)
Knuiman, Bart's avatar
Knuiman, Bart committed
                                    cachedTriIdx = triIdx;
                                    cachedVtxIdx = vIdx;
                                    cachedPixel  = (byte)feature.SelectedLayerIdx;
                                    cancel = callback( x, y, cachedPixel );
Knuiman, Bart's avatar
Knuiman, Bart committed
                            if (hit) break;
                        }

                        if ( !hit )
                        {
                            failedPixels.Add( new Vector3Int( x, y, 255 ) );
Knuiman, Bart's avatar
Knuiman, Bart committed

            return failedPixels;
Knuiman, Bart's avatar
Knuiman, Bart committed
    }

    public static class VectorTileLoader
    {
Knuiman, Bart's avatar
Knuiman, Bart committed
        public static VectorTile LoadFromUrl( string url, bool autoStart=true )
Knuiman, Bart's avatar
Knuiman, Bart committed
        {
            VectorTile tile = new VectorTile();
Knuiman, Bart's avatar
Knuiman, Bart committed
            tile.request = UnityWebRequest.Get( url );
            if (autoStart)
            {
                tile.StartDownload();
            }
Knuiman, Bart's avatar
Knuiman, Bart committed
            return tile;
        }
    }
}