/// <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); }
/// <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); }