/// <summary> /// Returns the smallest (most-important) element in a list, to identify which element a client should use. /// </summary> /// <param name="entriesHere">the list of elements to pull data from</param> /// <returns>the name, areatype, and client facing ID of the OSM element to use</returns> public static TerrainData DetermineAreaPlace(List <DbTables.Place> entriesHere) { //Which Place in this given Area is the one that should be displayed on the game/map as the name? picks the smallest one. //This one only returns the smallest entry, for games that only need to check the most interesting area in a cell. var entry = entriesHere.Last(); return(new TerrainData() { Name = TagParser.GetPlaceName(entry.Tags), areaType = entry.GameElementName, PrivacyId = entry.PrivacyId }); }
/// <summary> /// Scores the list of places given in full /// </summary> /// <param name="places">The places to report a score for each.</param> /// <returns>a string of names and scores, </returns> public static string GetScoresForFullArea(List <DbTables.Place> places) { //As above, but counts the Places' full area, not the area in the given Cell8 or Cell10. List <Tuple <string, long, Guid> > areaSizes = new List <Tuple <string, long, Guid> >(places.Count()); foreach (var place in places) { areaSizes.Add(Tuple.Create(TagParser.GetPlaceName(place.Tags), GetScoreForSinglePlace(place.ElementGeometry), place.PrivacyId)); } return(string.Join("\r\n", areaSizes.Select(a => a.Item1 + "|" + a.Item2 + "|" + a.Item3))); }
public void UpdateExistingEntries(List <DbTables.Place> entries) { foreach (var entry in entries) { //check existing entry, see if it requires being updated var existingData = Places.FirstOrDefault(md => md.SourceItemID == entry.SourceItemID && md.SourceItemType == entry.SourceItemType); if (existingData != null) { if (existingData.AreaSize != entry.AreaSize) { existingData.AreaSize = entry.AreaSize; } if (existingData.GameElementName != entry.GameElementName) { existingData.GameElementName = entry.GameElementName; } bool expireTiles = false; if (!existingData.ElementGeometry.EqualsTopologically(entry.ElementGeometry)) //TODO: this might need to be EqualsExact? { //update the geometry for this object. existingData.ElementGeometry = entry.ElementGeometry; expireTiles = true; } if (!existingData.Tags.OrderBy(t => t.Key).SequenceEqual(entry.Tags.OrderBy(t => t.Key))) { existingData.Tags = entry.Tags; var styleA = TagParser.GetStyleForOsmWay(existingData.Tags); var styleB = TagParser.GetStyleForOsmWay(entry.Tags); if (styleA != styleB) { expireTiles = true; //don't force a redraw on tags unless we change our drawing rules. } } if (expireTiles) //geometry or style has to change, otherwise we can skip expiring values. { SaveChanges(); //save before expiring, so the next redraw absolutely has the latest data. Can't catch it mid-command if we do this first. ExpireMapTiles(entry.ElementGeometry, "mapTiles"); ExpireSlippyMapTiles(entry.ElementGeometry, "mapTiles"); } } else { //We don't have this item, add it. Places.Add(entry); SaveChanges(); //again, necessary here to get tiles to draw correctly after expiring. ExpireMapTiles(entry.ElementGeometry, "mapTiles"); ExpireSlippyMapTiles(entry.ElementGeometry, "mapTiles"); } } SaveChanges(); //final one for anything not yet persisted. }
/// <summary> /// Draw square boxes around each area to approximate how they would behave in an offline app /// </summary> /// <param name="info">the image information for drawing</param> /// <param name="items">the elements to draw.</param> /// <returns>byte array of the generated .png tile image</returns> public byte[] DrawOfflineEstimatedAreas(ImageStats info, List <DbTables.Place> items) { SKBitmap bitmap = new SKBitmap(info.imageSizeX, info.imageSizeY, SKColorType.Rgba8888, SKAlphaType.Premul); SKCanvas canvas = new SKCanvas(bitmap); var bgColor = SKColors.Transparent; canvas.Clear(bgColor); canvas.Scale(1, -1, info.imageSizeX / 2, info.imageSizeY / 2); SKPaint fillpaint = new SKPaint(); fillpaint.IsAntialias = true; fillpaint.Style = SKPaintStyle.Fill; var strokePaint = new SKPaint(); strokePaint.Color = SKColors.Black; strokePaint.TextSize = 32; strokePaint.StrokeWidth = 3; strokePaint.Style = SKPaintStyle.Stroke; strokePaint.TextAlign = SKTextAlign.Center; //TagParser.ApplyTags(items); var placeInfo = PraxisCore.Standalone.Standalone.GetPlaceInfo(items.Where(i => i.IsGameElement ).ToList()); //this is for rectangles. foreach (var pi in placeInfo) { var rect = PlaceInfoToRect(pi, info); fillpaint.Color = SKColor.Parse(TagParser.PickStaticColorForArea(pi.Name)); canvas.DrawRect(rect, fillpaint); canvas.DrawRect(rect, strokePaint); } canvas.Scale(1, -1, info.imageSizeX / 2, info.imageSizeY / 2); //inverts the inverted image again! foreach (var pi in placeInfo) { var rect = PlaceInfoToRect(pi, info); canvas.DrawText(pi.Name, rect.MidX, info.imageSizeY - rect.MidY, strokePaint); } var ms = new MemoryStream(); var skms = new SKManagedWStream(ms); bitmap.Encode(skms, SKEncodedImageFormat.Png, 100); var results = ms.ToArray(); skms.Dispose(); ms.Close(); ms.Dispose(); return(results); }
/// <summary> /// Get the areatype (as defined by TagParser) for each OSM element in the list, along with name and client-facing ID. /// </summary> /// <param name="entriesHere">the list of elements to pull data from</param> /// <returns>A list of name, areatype, and elementIds for a client</returns> public static List <TerrainData> DetermineAreaPlaces(List <DbTables.Place> entriesHere) { //Which Place in this given Area is the one that should be displayed on the game/map as the name? picks the smallest one. //This one return all entries, for a game mode that might need all of them. var results = new List <TerrainData>(entriesHere.Count()); foreach (var e in entriesHere) { results.Add(new TerrainData() { Name = TagParser.GetPlaceName(e.Tags), areaType = e.GameElementName, PrivacyId = e.PrivacyId }); } return(results); }
//TODO: these might be slightly faster with StringBuilder during calculations than String.Join after doing all the calculation. Test and check that assumption. //Default Scoring rules: //Each Cell10 of surface area is 1 Score (would be Points in any other game, but Points is already an overloaded term in this system). //OSM Areas are measured in square area, divided by Cell10 area squared. (An area that covers 25 square Cell10s is 25 Score) //Lines are measured in their length. (A trail that's 25 * resolutionCell10 long is 25 Score) //OSM Points (single lat/lon pair) are assigned a Score of 1 as the minimum interactable size object. /// <summary> /// Get a list of element names, score, and client-facing IDs from a list of places and an area to search, cropped to that area. /// </summary> /// <param name="areaPoly">the area to search and use to determine scores of elements intersecting it </param> /// <param name="places">the elements to be scored, relative to their size in the given area</param> /// <returns>a string of pipe-separated values (name, score, ID) split by newlines</returns> public static string GetScoresForArea(Geometry areaPoly, List <DbTables.Place> places) { //Determines the Scores for the Places, limited to the intersection of the current Area. 1 Cell10 = 1 Score. //EX: if a park overlaps 800 Cell10s, but the current area overlaps 250 of them, this returns 250 for that park. //Lists each Place and its corresponding Score. List <Tuple <string, long, Guid> > areaSizes = new List <Tuple <string, long, Guid> >(places.Count()); foreach (var md in places) { var containedArea = md.ElementGeometry.Intersection(areaPoly); var areaCell10Count = GetScoreForSinglePlace(containedArea); areaSizes.Add(new Tuple <string, long, Guid>(TagParser.GetPlaceName(md.Tags), areaCell10Count, md.PrivacyId)); } return(string.Join("\r\n", areaSizes.Select(a => a.Item1 + "|" + a.Item2 + "|" + a.Item3))); }
/// <summary> /// Create a database StoredOsmElement from an OSMSharp Complete object. /// </summary> /// <param name="g">the CompleteOSMGeo object to prepare to save to the DB</param> /// <returns>the StoredOsmElement ready to save to the DB</returns> public static DbTables.Place ConvertOsmEntryToStoredElement(OsmSharp.Complete.ICompleteOsmGeo g) { var tags = TagParser.getFilteredTags(g.Tags); if (tags == null || tags.Count() == 0) { return(null); //For nodes, don't store every untagged node. } try { var geometry = featureInterpreter.Interpret(g); if (geometry == null) { Log.WriteLog("Error: " + g.Type.ToString() + " " + g.Id + "-" + TagParser.GetPlaceName(g.Tags) + " didn't interpret into a Geometry object", Log.VerbosityLevels.Errors); return(null); } var sw = new DbTables.Place(); sw.SourceItemID = g.Id; sw.SourceItemType = (g.Type == OsmGeoType.Relation ? 3 : g.Type == OsmGeoType.Way ? 2 : 1); var geo = SimplifyArea(geometry); if (geo == null) { Log.WriteLog("Error: " + g.Type.ToString() + " " + g.Id + " didn't simplify for some reason.", Log.VerbosityLevels.Errors); return(null); } geo.SRID = 4326;//Required for SQL Server to accept data. sw.ElementGeometry = geo; sw.Tags = tags; if (sw.ElementGeometry.GeometryType == "LinearRing" || (sw.ElementGeometry.GeometryType == "LineString" && sw.ElementGeometry.Coordinates.First() == sw.ElementGeometry.Coordinates.Last())) { //I want to update all LinearRings to Polygons, and let the style determine if they're Filled or Stroked. var poly = factory.CreatePolygon((LinearRing)sw.ElementGeometry); sw.ElementGeometry = poly; } sw.AreaSize = sw.ElementGeometry.Length > 0 ? sw.ElementGeometry.Length : resolutionCell10; return(sw); } catch (Exception ex) { Log.WriteLog("Error: Item " + g.Id + " failed to process. " + ex.Message); return(null); } }
/// <summary> /// Draw square boxes around each area to approximate how they would behave in an offline app /// </summary> /// <param name="info">the image information for drawing</param> /// <param name="items">the elements to draw.</param> /// <returns>byte array of the generated .png tile image</returns> public byte[] DrawOfflineEstimatedAreas(ImageStats info, List <DbTables.Place> items) { //TODO retest this. var image = new Image <Rgba32>(info.imageSizeX, info.imageSizeY); var bgColor = Rgba32.ParseHex("00000000"); image.Mutate(x => x.Fill(bgColor)); var fillColor = Rgba32.ParseHex("000000"); var strokeColor = Rgba32.ParseHex("000000"); var placeInfo = Standalone.Standalone.GetPlaceInfo(items.Where(i => i.IsGameElement ).ToList()); //this is for rectangles. foreach (var pi in placeInfo) { var rect = PlaceInfoToRect(pi, info); fillColor = Rgba32.ParseHex(TagParser.PickStaticColorForArea(pi.Name)); image.Mutate(x => x.Fill(fillColor, rect)); image.Mutate(x => x.Draw(strokeColor, 3, rect)); } image.Mutate(x => x.Flip(FlipMode.Vertical));; //inverts the inverted image again! foreach (var pi in placeInfo) { //NOTE: would be better to load fonts once and share that for the app's lifetime. var fonts = new SixLabors.Fonts.FontCollection(); var family = fonts.Add("fontHere.ttf"); var font = family.CreateFont(12, FontStyle.Regular); var rect = PlaceInfoToRect(pi, info); image.Mutate(x => x.DrawText(pi.Name, font, strokeColor, new PointF((float)(pi.lonCenter * info.pixelsPerDegreeX), (float)(pi.latCenter * info.pixelsPerDegreeY)))); } image.Mutate(x => x.Flip(FlipMode.Vertical)); //Plus codes are south-to-north, so invert the image to make it correct. var ms = new MemoryStream(); image.SaveAsPng(ms); return(ms.ToArray()); }
//Places will be the name for interactible or important areas on the map. Generally, a Place in the DB. //(vs Area, which is a PlusCode of any size) //All elements in the table with Geometry will be valid, and the TagParser rules will determine which ones are game elements //this allows it to be customized much easier, and changed on the fly without reloading data. /// <summary> /// The core for pulling in locations from PraxisMapper. Can do a new search on an existing list of Place or pulls from the database if none is provided. Adds padding as set from config automatically. /// </summary> /// <param name="area">The GeoArea to intersect locations against, and include ones that do. </param> /// <param name="source">Null to load from the database, or a List of Places to narrow down</param> /// <param name="filterSize">Removes any areas with a length or perimeter over this value. Defaults to 0 to include everything.</param> /// <param name="styleSet">A style set to run the found locations through for identification.</param> /// <param name="skipTags">If true, skips over tagging elements. A performance boost when you have a List to narrow down already.</param> /// <param name="includePoints">If false, removes Points from the source before returning the results</param> /// <returns>A list of Places that intersect the area, have a perimter greater than or equal to filtersize.</returns> public static List <DbTables.Place> GetPlaces(GeoArea area, List <DbTables.Place> source = null, double filterSize = 0, string styleSet = "mapTiles", bool skipTags = false, bool includePoints = true) { //parameters i will need to restore later. //bool includeGenerated = false; //The flexible core of the lookup functions. Takes an area, returns results that intersect from Source. If source is null, looks into the DB. //Intersects is the only indexable function on a geography column I would want here. Distance and Equals can also use the index, but I don't need those in this app. List <DbTables.Place> places; if (source == null) { var paddedArea = GeometrySupport.MakeBufferedGeoArea(area); var location = Converters.GeoAreaToPolygon(paddedArea); //Prepared items don't work on a DB lookup. var db = new PraxisContext(); db.Database.SetCommandTimeout(new TimeSpan(0, 5, 0)); if (skipTags) //Should make the load slightly faster if we're parsing existing items that already got tags applied { places = db.Places.Where(md => location.Intersects(md.ElementGeometry) && md.AreaSize >= filterSize && (includePoints || md.SourceItemType != 1)).OrderByDescending(w => w.ElementGeometry.Area).ThenByDescending(w => w.ElementGeometry.Length).ToList(); return(places); //Jump out before we do ApplyTags } else { places = db.Places.Include(s => s.Tags).Where(md => location.Intersects(md.ElementGeometry) && md.AreaSize >= filterSize && (includePoints || md.SourceItemType != 1)).OrderByDescending(w => w.ElementGeometry.Area).ThenByDescending(w => w.ElementGeometry.Length).ToList(); } } else { var location = Converters.GeoAreaToPreparedPolygon(area); places = source.Where(md => location.Intersects(md.ElementGeometry) && md.AreaSize >= filterSize && (includePoints || md.SourceItemType != 1)).Select(md => md.Clone()).ToList(); } if (!skipTags) { TagParser.ApplyTags(places, styleSet); //populates the fields we don't save to the DB. } return(places); }
/// <summary> /// A debugging function, writres some information an element using its OSM id (or internal primary key) to load from the database. /// </summary> /// <param name="id">the PlaceId or SourceElementId of an area to load.</param> /// <returns>a string with some details on the area in question.</returns> public static string LoadDataOnPlace(long id) { //Debugging helper call. Loads up some information on an area and display it. //Not currently used anywhere. var db = new PraxisContext(); var entries = db.Places.Where(m => m.Id == id || m.SourceItemID == id).ToList(); string results = ""; foreach (var entry in entries) { var shape = entry.ElementGeometry; results += "Name: " + TagParser.GetPlaceName(entry.Tags) + Environment.NewLine; results += "Game Type: " + TagParser.GetAreaType(entry.Tags.ToList()) + Environment.NewLine; results += "Geometry Type: " + shape.GeometryType + Environment.NewLine; results += "OSM Type: " + entry.SourceItemType + Environment.NewLine; results += "Area Tags: " + String.Join(",", entry.Tags.Select(t => t.Key + ":" + t.Value)); results += "IsValid? : " + shape.IsValid + Environment.NewLine; results += "Area: " + shape.Area + Environment.NewLine; //Not documented, but I believe this is the area in square degrees. Is that a real unit? results += "As Text: " + shape.AsText() + Environment.NewLine; } return(results); }
} = Guid.NewGuid(); //Pass this Id to clients, so we can attempt to block attaching players to locations in the DB. public override string ToString() { return((SourceItemType == 3 ? "Relation " : SourceItemType == 2 ? "Way " : "Node ") + SourceItemID.ToString() + TagParser.GetPlaceName(Tags)); }