Example #1
0
        /// <summary>
        /// Auto-generate some places to be used as gameplay elements in otherwise sparse areas. Given an 8 digit PlusCode, creates and warps some standard shapes in the Cell.
        /// </summary>
        /// <param name="plusCode">The area to generate shape(s) in</param>
        /// <param name="autoSave">If true, saves the areas to the database immediately.</param>
        /// <returns>The list of places created for the given area.</returns>
        public static List <DbTables.Place> CreateInterestingPlaces(string plusCode, bool autoSave = true)
        {
            //expected to receive a Cell8
            // populate it with some interesting regions for players.
            Random   r          = new Random();
            CodeArea cell8      = OpenLocationCode.DecodeValid(plusCode); //Reminder: area is .0025 degrees on a Cell8
            int      shapeCount = 1;                                      // 2; //number of shapes to apply to the Cell8
            double   shapeWarp  = .3;                                     //percentage a shape is allowed to have each vertexs drift by.
            List <DbTables.Place> areasToAdd = new List <DbTables.Place>();

            for (int i = 0; i < shapeCount; i++)
            {
                //Pick a shape
                var masterShape     = possibleShapes.OrderBy(s => r.Next()).First();
                var shapeToAdd      = masterShape.Select(s => new Coordinate(s)).ToList();
                var scaleFactor     = r.Next(10, 36) * .01; //Math.Clamp(r.Next, .1, .35); //Ensure that we get a value that isn't terribly useless. 2 shapes can still cover 70%+ of an empty area this way.
                var positionFactorX = r.NextDouble() * resolutionCell8;
                var positionFactorY = r.NextDouble() * resolutionCell8;
                foreach (Coordinate c in shapeToAdd)
                {
                    //scale it to our resolution
                    c.X *= resolutionCell8;
                    c.Y *= resolutionCell8;

                    //multiply this by some factor smaller than 1, so it doesn't take up the entire Cell
                    //If we use NextDouble() here, it scales each coordinate randomly, which would look very unpredictable. Use the results of one call twice to scale proportionally.
                    //but ponder how good/bad it looks for various shapes if each coordinate is scaled differently.
                    c.X *= scaleFactor;
                    c.Y *= scaleFactor;

                    //Rotate the coordinate set some random number of degrees?
                    //TODO: how to rotate these?

                    //Place the shape somewhere randomly by adding the same X/Y value less than the resolution to each point
                    c.X += positionFactorX;
                    c.Y += positionFactorY;

                    //Fuzz each vertex by adding some random distance on each axis less than 30% of the cell's size in either direction.
                    //10% makes the shapes much more recognizable, but not as interesting. Will continue looking into parameters here to help adjust that.
                    c.X += (r.NextDouble() * resolutionCell8 * shapeWarp) * (r.Next() % 2 == 0 ? 1 : -1);
                    c.Y += (r.NextDouble() * resolutionCell8 * shapeWarp) * (r.Next() % 2 == 0 ? 1 : -1);

                    //Let us know if this shape overlaps a neighboring cell. We probably want to make sure we re-draw map tiles if it does.
                    if (c.X > .0025 || c.Y > .0025)
                    {
                        Log.WriteLog("Coordinate for shape " + i + " in Cell8 " + plusCode + " will be out of bounds: " + c.X + " " + c.Y, Log.VerbosityLevels.High);
                    }

                    //And now add the minimum values for the given Cell8 to finish up coordinates.
                    c.X += cell8.Min.Longitude;
                    c.Y += cell8.Min.Latitude;
                }

                //ShapeToAdd now has a randomized layout, convert it to a polygon.
                shapeToAdd.Add(shapeToAdd.First()); //make it a closed shape
                var polygon = factory.CreatePolygon(shapeToAdd.ToArray());
                polygon = CCWCheck(polygon);        //Sometimes squares still aren't CCW? or this gets un-done somewhere later?
                if (!polygon.IsValid || !polygon.Shell.IsCCW)
                {
                    Log.WriteLog("Invalid geometry generated, retrying", Log.VerbosityLevels.High);
                    i--;
                    continue;
                }

                if (!polygon.CoveredBy(Converters.GeoAreaToPolygon(cell8)))
                {
                    //This should only ever require checking the map tile north/east of the current one, even though the vertex fuzzing can potentially move things negative slightly.
                    Log.WriteLog("This polygon is outside of the Cell8 by " + (cell8.Max.Latitude - shapeToAdd.Max(s => s.Y)) + "/" + (cell8.Max.Longitude - shapeToAdd.Max(s => s.X)), Log.VerbosityLevels.High);
                }
                if (polygon != null)
                {
                    DbTables.Place gmd = new DbTables.Place();
                    gmd.ElementGeometry = polygon;
                    gmd.GameElementName = "generated";
                    gmd.Tags.Add(new PlaceTags()
                    {
                        Key = "praxisGenerated", Value = "true"
                    });
                    areasToAdd.Add(gmd); //this is the line that makes some objects occasionally not be CCW that were CCW before. Maybe its the cast to the generic Geometry item?
                }
                else
                {
                    //Inform me that I did something wrong.
                    Log.WriteLog("failed to convert a randomized shape to a polygon.", Log.VerbosityLevels.Errors);
                    continue;
                }
            }

            //Making this function self-contained
            if (autoSave)
            {
                var db = new PraxisContext();
                foreach (var area in areasToAdd)
                {
                    area.ElementGeometry = CCWCheck((Polygon)area.ElementGeometry); //fixes errors that reappeared above
                }
                db.Places.AddRange(areasToAdd);
                db.SaveChanges();
            }

            return(areasToAdd);
        }
Example #2
0
        /// <summary>
        /// Creates an SVG image instead of a PNG file, but otherwise operates the same as DrawAreaAtSize.
        /// </summary>
        /// <param name="stats">the image properties to draw</param>
        /// <param name="drawnItems">the list of elements to draw. Will load from the database if null.</param>
        /// <param name="styles">a dictionary of TagParserEntries to select to draw</param>
        /// <param name="filterSmallAreas">if true, skips entries below a certain size when drawing.</param>
        /// <returns>a string containing the SVG XML</returns>
        public string DrawAreaAtSizeSVG(ImageStats stats, List <DbTables.Place> drawnItems = null, Dictionary <string, StyleEntry> styles = null, bool filterSmallAreas = true)
        {
            //TODO: make this take CompletePaintOps
            //This is the new core drawing function. Takes in an area, the items to draw, and the size of the image to draw.
            //The drawn items get their paint pulled from the TagParser's list. If I need multiple match lists, I'll need to make a way
            //to pick which list of tagparser rules to use.

            if (styles == null)
            {
                styles = TagParser.allStyleGroups.First().Value;
            }

            double minimumSize = 0;

            if (filterSmallAreas)
            {
                minimumSize = stats.degreesPerPixelX; //don't draw elements under 1 pixel in size. at slippy zoom 12, this is approx. 1 pixel for a Cell10.
            }
            var db  = new PraxisContext();
            var geo = Converters.GeoAreaToPolygon(stats.area);

            if (drawnItems == null)
            {
                drawnItems = GetPlaces(stats.area, filterSize: minimumSize);
            }

            //baseline image data stuff
            //SKBitmap bitmap = new SKBitmap(stats.imageSizeX, stats.imageSizeY, SKColorType.Rgba8888, SKAlphaType.Premul);
            var          bounds = new SKRect(0, 0, stats.imageSizeX, stats.imageSizeY);
            MemoryStream s      = new MemoryStream();
            SKCanvas     canvas = SKSvgCanvas.Create(bounds, s); //output not guaranteed to be complete until the canvas is deleted?!?
            //SKCanvas canvas = new SKCanvas(bitmap);
            var bgColor = SKColor.Parse(styles["background"].PaintOperations.First().HtmlColorCode);

            //Backgound is a named style, unmatched will be the last entry and transparent.
            canvas.Clear(bgColor);
            canvas.Scale(1, -1, stats.imageSizeX / 2, stats.imageSizeY / 2);
            SKPaint paint = new SKPaint();

            //I guess what I want here is a list of an object with an elementGeometry object for the shape, and a paintOp attached to it
            var pass1 = drawnItems.Select(d => new { d.AreaSize, d.ElementGeometry, paintOp = styles[d.GameElementName].PaintOperations });
            var pass2 = new List <CompletePaintOp>(drawnItems.Count() * 2);

            foreach (var op in pass1)
            {
                foreach (var po in op.paintOp)
                {
                    pass2.Add(new CompletePaintOp(op.ElementGeometry, op.AreaSize, po, "", po.LineWidthDegrees * stats.pixelsPerDegreeX));
                }
            }


            foreach (var w in pass2.OrderByDescending(p => p.paintOp.LayerId).ThenByDescending(p => p.areaSize))
            {
                paint = cachedPaints[w.paintOp.Id];
                if (paint.Color.Alpha == 0)
                {
                    continue; //This area is transparent, skip drawing it entirely.
                }
                if (stats.degreesPerPixelX > w.paintOp.MaxDrawRes || stats.degreesPerPixelX < w.paintOp.MinDrawRes)
                {
                    continue; //This area isn't drawn at this scale.
                }
                var path = new SKPath();
                switch (w.elementGeometry.GeometryType)
                {
                //Polygons without holes are super easy and fast: draw the path.
                //Polygons with holes require their own bitmap to be drawn correctly and then overlaid onto the canvas.
                //I want to use paths to fix things for performance reasons, but I have to use Bitmaps because paths apply their blend mode to
                //ALL elements already drawn, not just the last one.
                case "Polygon":
                    var p = w.elementGeometry as Polygon;

                    path.AddPoly(PolygonToSKPoints(p, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY));
                    foreach (var hole in p.InteriorRings)
                    {
                        path.AddPoly(PolygonToSKPoints(hole, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY));
                    }
                    canvas.DrawPath(path, paint);

                    break;

                case "MultiPolygon":
                    foreach (var p2 in ((MultiPolygon)w.elementGeometry).Geometries)
                    {
                        var p2p = p2 as Polygon;
                        path.AddPoly(PolygonToSKPoints(p2p, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY));
                        foreach (var hole in p2p.InteriorRings)
                        {
                            path.AddPoly(PolygonToSKPoints(hole, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY));
                        }
                        canvas.DrawPath(path, paint);
                    }
                    break;

                case "LineString":
                    var firstPoint = w.elementGeometry.Coordinates.First();
                    var lastPoint  = w.elementGeometry.Coordinates.Last();
                    var points     = PolygonToSKPoints(w.elementGeometry, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY);
                    if (firstPoint.Equals(lastPoint))
                    {
                        //This is a closed shape. Check to see if it's supposed to be filled in.
                        if (paint.Style == SKPaintStyle.Fill)
                        {
                            path.AddPoly(points);
                            canvas.DrawPath(path, paint);
                            continue;
                        }
                    }
                    for (var line = 0; line < points.Length - 1; line++)
                    {
                        canvas.DrawLine(points[line], points[line + 1], paint);
                    }
                    break;

                case "MultiLineString":
                    foreach (var p3 in ((MultiLineString)w.elementGeometry).Geometries)
                    {
                        var points2 = PolygonToSKPoints(p3, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY);
                        for (var line = 0; line < points2.Length - 1; line++)
                        {
                            canvas.DrawLine(points2[line], points2[line + 1], paint);
                        }
                    }
                    break;

                case "Point":
                    var convertedPoint = PolygonToSKPoints(w.elementGeometry, stats.area, stats.degreesPerPixelX, stats.degreesPerPixelY);
                    //If this type has an icon, use it. Otherwise draw a circle in that type's color.
                    if (!string.IsNullOrEmpty(w.paintOp.FileName))
                    {
                        SKBitmap icon = SKBitmap.Decode(TagParser.cachedBitmaps[w.paintOp.FileName]);     //TODO optimize by creating in Initialize
                        canvas.DrawBitmap(icon, convertedPoint[0]);
                    }
                    else
                    {
                        var circleRadius = (float)(ConstantValues.resolutionCell10 / stats.degreesPerPixelX / 2);     //I want points to be drawn as 1 Cell10 in diameter.
                        canvas.DrawCircle(convertedPoint[0], circleRadius, paint);
                    }
                    break;

                default:
                    Log.WriteLog("Unknown geometry type found, not drawn.");
                    break;
                }
            }
            canvas.Flush();
            canvas.Dispose();
            canvas     = null;
            s.Position = 0;
            var svgData = new StreamReader(s).ReadToEnd();

            return(svgData);
        }