Skip to content
Snippets Groups Projects
VectorTile.cs 7.69 KiB
Newer Older
using Codice.Client.Common.FsNodeReaders;
using Mapbox.Vector.Tile;
using System.Collections.Generic;
using System.IO;
using System.Threading.Tasks;
Knuiman, Bart's avatar
Knuiman, Bart committed
using UnityEngine.Networking;

namespace Wander
{
    // Always multiple of 3. Each 3 form a triangle.
    public struct TriangulatedPolygon
Knuiman, Bart's avatar
Knuiman, Bart committed
    public class VectorTile
    {
        public bool Valid => valid;
        public bool Finished => finished;
        public bool Triangulated => triangulated;
        private bool valid;
        private bool finished;
        private bool triangulated;
        private Task parseTileTask;
        private List<VectorTileLayer> layers;
        private List<List<TriangulatedPolygon>> polygonLayers;
        internal UnityWebRequestAsyncOperation request;
        {
            if (finished)
                return true;

            if (!request.isDone)
                return false;

            if (request.webRequest.result == UnityWebRequest.Result.Success)
            {
                if (parseTileTask == null)
                {
                    parseTileTask = Task.Run( () =>
                    {
                        var stream = new MemoryStream( request.webRequest.downloadHandler.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++)
                {
                    if (features[j].GeometryType != Tile.GeomType.Polygon)
                        continue;
                    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 );
                        for (int k = 0;k < indices.Count;k+= 3)
                        {
                            double x = vertices[indices[k]*2];
                            double y = vertices[indices[k]*2+1];
                            poly.vertices.Add( new Vector2( (float)x, (float)y ) );
                        }
                        polygons.Add( poly );
                        Debug.Assert( poly.vertices.Count % 3 == 0 );
                    }
                    catch (Exception)
                    {
                        numFailedPolys++;
                    }
                }
                polygonLayers.Add( polygons );
            }
            triangulated = true;
            return numFailedPolys;
        }

        // Returns an R8 texture (256 channels) where each value represents a single channel.
        // Retrieve back in shader using: texIdx = floor( texture.r*255+0.5 ).
        // If no polygon was hit, 255 is encoded to the texture.
        // If polygon was hit, but no feature was matched, 254 is encoded.
        public Texture2D RenderToTextureSingle( int width, int height, List<string> featureIds )
        {
            Debug.Assert( triangulated, "First call Triangulate." );
            Debug.Assert( featureIds.Count < 254, "254 and 255 are reserved for no hit or no match." );
            byte [] pixels = new byte[width*height];
            int addr = 0;
            for ( int y = 0; y < height; y++)
            {
                for ( int x = 0; x < width; x++ )
                {
                    Vector2 p  = new Vector2(x, y);

                    for ( int l = 0; l < layers.Count; l++ )
                    {
                        var layer = layers[l];
                        for ( int f = 0; f < layer.VectorTileFeatures.Count; f ++ )
                        {
                            var feature = layer.VectorTileFeatures[f];

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

                            var triangles = polygonLayers[l][f].vertices;
                            bool hit   = false;
                            bool match = false;
                            for ( int i = 0; i < triangles.Count; i += 3 )
                            {
                                // Cool part is, that geometry is in local space relative to a grid starting at: 0,0 which matches with the texture raster.
                                // No conversions necessary.
                                hit = GeomUtil.PointIsInsideTriangle( p, triangles[i], triangles[i+1], triangles[i+2] );
                                if ( hit )
                                {
                                    for (int ids = 0; ids < featureIds.Count; ids++ )
                                    {
                                        if (feature.Id.Contains( featureIds[ids] ))
                                        {
                                            match = true;
                                            pixels[addr] = (byte) ids;
                                            break;
                                        }
                                    }
                                    break;
                                }
                            }

                            if ( !hit )
                            {
                                pixels[addr] = 255;
                            }
                            else if (!match) // hit but no match
                            {
                                pixels[addr] = 254;
                            }
                        }
                    }
            Texture2D t = new Texture2D(width, height, TextureFormat.R8, false);
            t.SetPixelData( pixels, 0, 0 );
            return t;
Knuiman, Bart's avatar
Knuiman, Bart committed
    }

    public static class VectorTileLoader
    {
        public static VectorTile LoadFromUrl( string url )
        {
            VectorTile tile = new VectorTile();
            tile.request = UnityWebRequest.Get( url ).SendWebRequest();
            return tile;
        }
    }
}