public static bool SetAreaData(string plusCode, string key, string value, double?expiration = null) { var db = new PraxisContext(); if (db.PlayerData.Any(p => p.DeviceID == key || p.DeviceID == value)) { return(false); } var row = db.AreaGameData.FirstOrDefault(p => p.PlusCode == plusCode && p.DataKey == key); if (row == null) { row = new DbTables.AreaGameData(); row.DataKey = key; row.PlusCode = plusCode; row.GeoAreaIndex = Converters.GeoAreaToPolygon(OpenLocationCode.DecodeValid(plusCode.ToUpper())); db.AreaGameData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.IvData = null; row.DataValue = value.ToByteArrayUTF8(); return(db.SaveChanges() == 1); }
/// <summary> /// Generates all SlippyMap tiles for a given area and zoom level, and saves them to the database. /// </summary> /// <param name="buffered">the GeoArea to generate tiles for</param> /// <param name="zoomLevel">the zoom level to generate tiles at.</param> public static void PregenSlippyMapTilesForArea(GeoArea buffered, int zoomLevel) { //There is a very similar function for this in Standalone.cs, but this one writes back to the main DB. var db = new PraxisContext(); db.ChangeTracker.AutoDetectChangesEnabled = false; var intersectCheck = Converters.GeoAreaToPolygon(buffered); //start drawing maptiles and sorting out data. var swCornerLon = Converters.GetSlippyXFromLon(intersectCheck.EnvelopeInternal.MinX, zoomLevel); var neCornerLon = Converters.GetSlippyXFromLon(intersectCheck.EnvelopeInternal.MaxX, zoomLevel); var swCornerLat = Converters.GetSlippyYFromLat(intersectCheck.EnvelopeInternal.MinY, zoomLevel); var neCornerLat = Converters.GetSlippyYFromLat(intersectCheck.EnvelopeInternal.MaxY, zoomLevel); //declare how many map tiles will be drawn var xTiles = neCornerLon - swCornerLon + 1; var yTiles = swCornerLat - neCornerLat + 1; var totalTiles = xTiles * yTiles; Log.WriteLog("Starting processing " + totalTiles + " maptiles for zoom level " + zoomLevel); long mapTileCounter = 0; System.Diagnostics.Stopwatch progressTimer = new System.Diagnostics.Stopwatch(); progressTimer.Start(); //foreach (var y in yCoords) for (var y = neCornerLat; y <= swCornerLat; y++) { //Make a collision box for just this row of Cell8s, and send the loop below just the list of things that might be relevant. //Add a Cell8 buffer space so all elements are loaded and drawn without needing to loop through the entire area. GeoArea thisRow = new GeoArea(Converters.SlippyYToLat(y + 1, zoomLevel) - ConstantValues.resolutionCell8, Converters.SlippyXToLon(swCornerLon, zoomLevel) - ConstantValues.resolutionCell8, Converters.SlippyYToLat(y, zoomLevel) + ConstantValues.resolutionCell8, Converters.SlippyXToLon(neCornerLon, zoomLevel) + resolutionCell8); var row = Converters.GeoAreaToPolygon(thisRow); var rowList = GetPlaces(thisRow); var tilesToSave = new ConcurrentBag <SlippyMapTile>(); Parallel.For(swCornerLon, neCornerLon + 1, (x) => { //make map tile. var info = new ImageStats(zoomLevel, x, y, IMapTiles.SlippyTileSizeSquare); var acheck = Converters.GeoAreaToPolygon(info.area); //this is faster than using a PreparedPolygon in testing, which was unexpected. var areaList = rowList.Where(a => acheck.Intersects(a.ElementGeometry)).ToList(); //This one is for the maptile var tile = MapTiles.DrawAreaAtSize(info, areaList); tilesToSave.Add(new SlippyMapTile() { TileData = tile, Values = x + "|" + y + "|" + zoomLevel, ExpireOn = DateTime.Now.AddDays(3650), AreaCovered = Converters.GeoAreaToPolygon(info.area), StyleSet = "mapTiles" }); mapTileCounter++; }); db.SlippyMapTiles.AddRange(tilesToSave); db.SaveChanges(); Log.WriteLog(mapTileCounter + " tiles processed, " + Math.Round(((mapTileCounter / (double)totalTiles * 100)), 2) + "% complete"); }//); progressTimer.Stop(); Log.WriteLog("Zoom " + zoomLevel + " map tiles drawn in " + progressTimer.Elapsed.ToString()); }
/// <summary> /// Saves a key/value pair to a given map element with the given password /// </summary> /// <param name="elementId">the Guid exposed to clients to identify the map element.</param> /// <param name="key">The key to save to the database for the map element.</param> /// <param name="value">The value to save with the key.</param> /// <param name="password">The password to encrypt the value with.</param> /// <param name="expiration">If not null, expire this data in this many seconds from now.</param> /// <returns>true if data was saved, false if data was not.</returns> public static bool SetSecurePlaceData(Guid elementId, string key, byte[] value, string password, double?expiration = null) { byte[] encryptedValue = EncryptValue(value, password, out byte[] IVs); var db = new PraxisContext(); var row = db.PlaceGameData.Include(p => p.Place).FirstOrDefault(p => p.Place.PrivacyId == elementId && p.DataKey == key); if (row == null) { var sourceItem = db.Places.First(p => p.PrivacyId == elementId); row = new DbTables.PlaceGameData(); row.DataKey = key; row.Place = sourceItem; db.PlaceGameData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.IvData = IVs; row.DataValue = encryptedValue; return(db.SaveChanges() == 1); }
/// <summary> /// Saves a key/value pair to a given map element. Will reject a pair containing a player's deviceId in the database. /// </summary> /// <param name="elementId">the Guid exposed to clients to identify the map element.</param> /// <param name="key">The key to save to the database for the map element.</param> /// <param name="value">The value to save with the key.</param> /// <param name="expiration">If not null, expire the data in this many seconds from now.</param> /// <returns>true if data was saved, false if data was not.</returns> public static bool SetPlaceData(Guid elementId, string key, string value, double?expiration = null) { var db = new PraxisContext(); if (db.PlayerData.Any(p => p.DeviceID == key || p.DeviceID == value)) { return(false); } var row = db.PlaceGameData.Include(p => p.Place).FirstOrDefault(p => p.Place.PrivacyId == elementId && p.DataKey == key); if (row == null) { var sourceItem = db.Places.First(p => p.PrivacyId == elementId); row = new DbTables.PlaceGameData(); row.DataKey = key; row.Place = sourceItem; db.PlaceGameData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.IvData = null; row.DataValue = value.ToByteArrayUTF8(); return(db.SaveChanges() == 1); }
public static bool SetSecureAreaData(string plusCode, string key, byte[] value, string password, double?expiration = null) { var db = new PraxisContext(); byte[] encryptedValue = EncryptValue(value, password, out byte[] IVs); var row = db.AreaGameData.FirstOrDefault(p => p.PlusCode == plusCode && p.DataKey == key); if (row == null) { row = new DbTables.AreaGameData(); row.DataKey = key; row.PlusCode = plusCode; row.GeoAreaIndex = Converters.GeoAreaToPolygon(OpenLocationCode.DecodeValid(plusCode.ToUpper())); db.AreaGameData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.DataValue = encryptedValue; row.IvData = IVs; return(db.SaveChanges() == 1); }
/// <summary> /// Saves a key/value pair to a given player's DeviceID with a password. /// </summary> /// <param name="playerId">the unique ID for the player, expected to be their unique device ID</param> /// <param name="key">The key to save to the database. Keys are unique, and you cannot have multiples of the same key.</param> /// <param name="value">The value to save with the key.</param> /// <param name="password"></param> /// <param name="expiration">If not null, expire this data in this many seconds from now.</param> /// <returns>true if data was saved, false if data was not.</returns> public static bool SetSecurePlayerData(string playerId, string key, byte[] value, string password, double?expiration = null) { var encryptedValue = EncryptValue(value, password, out byte[] IVs); var db = new PraxisContext(); var row = db.PlayerData.FirstOrDefault(p => p.DeviceID == playerId && p.DataKey == key); if (row == null) { row = new DbTables.PlayerData(); row.DataKey = key; row.DeviceID = playerId; db.PlayerData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.IvData = IVs; row.DataValue = encryptedValue; return(db.SaveChanges() == 1); }
/// <summary> /// Saves a key/value pair to a given player's DeviceID. Will reject a pair containing a PlusCode or map element Id. /// </summary> /// <param name="playerId"></param> /// <param name="key">The key to save to the database. Keys are unique, and you cannot have multiples of the same key.</param> /// <param name="value">The value to save with the key.</param> /// <param name="expiration">If not null, expire this data in this many seconds from now.</param> /// <returns>true if data was saved, false if data was not.</returns> public static bool SetPlayerData(string playerId, string key, string value, double?expiration = null) { if (DataCheck.IsPlusCode(key) || DataCheck.IsPlusCode(value)) { return(false); //Reject attaching a player to a pluscode. } var db = new PraxisContext(); Guid tempCheck = new Guid(); if ((Guid.TryParse(key, out tempCheck) && db.Places.Any(osm => osm.PrivacyId == tempCheck)) || (Guid.TryParse(value, out tempCheck) && db.Places.Any(osm => osm.PrivacyId == tempCheck))) { return(false); //reject attaching a player to an area } var row = db.PlayerData.FirstOrDefault(p => p.DeviceID == playerId && p.DataKey == key); if (row == null) { row = new DbTables.PlayerData(); row.DataKey = key; row.DeviceID = playerId; db.PlayerData.Add(row); } if (expiration.HasValue) { row.Expiration = DateTime.Now.AddSeconds(expiration.Value); } else { row.Expiration = null; } row.IvData = null; row.DataValue = value.ToByteArrayUTF8(); return(db.SaveChanges() == 1); }
/// <summary> /// Get the score for an area by its client facing ID /// </summary> /// <param name="elementId">the PrivacyID of a Place to be scored</param> /// <returns>the score for the requested Place</returns> public static long GetScoreForSinglePlace(Guid elementId) { var db = new PraxisContext(); var place = db.Places.FirstOrDefault(e => e.PrivacyId == elementId).ElementGeometry; return(GetScoreForSinglePlace(place)); }
/// <summary> /// Get the value from a key/value pair saved on a map element. Expired entries will be ignored. /// </summary> /// <param name="elementId">the Guid exposed to clients to identify the map element.</param> /// <param name="key">The key to load from the database. Keys are unique, and you cannot have multiples of the same key.</param> /// <returns>The value saved to the key, or an empty string if no key/value pair was found.</returns> public static byte[] GetSecurePlaceData(Guid elementId, string key, string password) { var db = new PraxisContext(); var row = db.PlaceGameData.Include(p => p.Place).FirstOrDefault(p => p.Place.PrivacyId == elementId && p.DataKey == key); if (row == null || row.Expiration.GetValueOrDefault(DateTime.MaxValue) < DateTime.Now) { return(new byte[0]); } return(DecryptValue(row.IvData, row.DataValue, password)); }
/// <summary> /// Get the value from a key/value pair saved on a player's deviceId encrypted with the given password. Expired entries will be ignored. /// </summary> /// <param name="playerId">the player-specific ID used. Expected to be a unique DeviceID to identify a phone, per that device's rules.</param> /// <param name="key">The key to load data from for the playerId.</param> /// <param name="password">The password used to encrypt the value originally.</param> /// <returns>The value saved to the key with the password given, or an empty string if no key/value pair was found or the password is incorrect.</returns> public static byte[] GetSecurePlayerData(string playerId, string key, string password) { var db = new PraxisContext(); var row = db.PlayerData.FirstOrDefault(p => p.DeviceID == playerId && p.DataKey == key); if (row == null || row.Expiration.GetValueOrDefault(DateTime.MaxValue) < DateTime.Now) { return(new byte[0]); } return(DecryptValue(row.IvData, row.DataValue, password)); }
/// <summary> /// Get the value from a key/value pair saved on a PlusCode. Expired values will not be sent over. /// </summary> /// <param name="plusCode">A valid PlusCode, excluding the + symbol.</param> /// <param name="key">The key to load from the database on the PlusCode</param> /// <returns>The value saved to the key, or an empty byte[] if no key/value pair was found.</returns> public static byte[] GetAreaData(string plusCode, string key) { var db = new PraxisContext(); var row = db.AreaGameData.FirstOrDefault(p => p.PlusCode == plusCode && p.DataKey == key); if (row == null || row.Expiration.GetValueOrDefault(DateTime.MaxValue) < DateTime.Now) { return(new byte[0]); } return(row.DataValue); }
/// <summary> /// Loads a key/value pair from the database that isn't attached to anything specific. Global entries do not expire. /// </summary> /// <param name="key">The key to load data from.</param> /// <returns>The value saved to the key, or an empty string if no key/value pair was found.</returns> public static byte[] GetGlobalData(string key) { var db = new PraxisContext(); var row = db.GlobalDataEntries.FirstOrDefault(s => s.DataKey == key); if (row == null) { return(new byte[0]); } return(row.DataValue); }
/// <summary> /// Returns all data attached to a player's device ID /// </summary> /// <param name="deviceID">the device associated with a player</param> /// <returns>List of reuslts with deviceId, keys, and values</returns> public static List <CustomDataPlayerResult> GetAllPlayerData(string deviceID) { var db = new PraxisContext(); var data = db.PlayerData.Where(p => p.DeviceID == deviceID) .ToList() .Where(row => row.Expiration.GetValueOrDefault(DateTime.MaxValue) > DateTime.Now && row.IvData == null) .Select(d => new CustomDataPlayerResult(d.DeviceID, d.DataKey, d.DataValue.ToUTF8String())) .ToList(); return(data); }
/// <summary> /// Load all of the key/value pairs attached to a map element. Expired entries will be ignored. /// </summary> /// <param name="elementId">the Guid exposed to clients to identify the map element.</param> /// /// <param name="key">If supplied, only returns data on the given key for the area provided. If blank, returns all keys</param> /// <returns>a List of results with the map element ID, keys, and values</returns> public static List <CustomDataPlaceResult> GetAllDataInPlace(Guid elementId, string key = "") { var db = new PraxisContext(); var place = db.Places.First(s => s.PrivacyId == elementId); var data = db.PlaceGameData.Where(d => d.PlaceId == place.Id && (key == "" || d.DataKey == key) && d.IvData == null) .ToList() //Required to run the next Where on the C# side .Where(row => row.Expiration.GetValueOrDefault(DateTime.MaxValue) > DateTime.Now) .Select(d => new CustomDataPlaceResult(place.PrivacyId, d.DataKey, d.DataValue.ToUTF8String())) .ToList(); return(data); }
/// <summary> /// Load all of the key/value pairs in a PlusCode, including pairs saved to a longer PlusCode. Expired and encrypted entries are ignored. /// (EX: calling this with an 8 character PlusCode will load all contained 10 character PlusCodes key/value pairs) /// </summary> /// <param name="plusCode">A valid PlusCode, excluding the + symbol.</param> /// <param name="key">If supplied, only returns data on the given key for the area provided. If blank, returns all keys</param> /// <returns>a List of results with the PlusCode, keys, and values</returns> public static List <CustomDataAreaResult> GetAllDataInArea(string plusCode, string key = "") { var db = new PraxisContext(); var plusCodeArea = OpenLocationCode.DecodeValid(plusCode); var plusCodePoly = Converters.GeoAreaToPolygon(plusCodeArea); var plusCodeData = db.AreaGameData.Where(d => plusCodePoly.Intersects(d.GeoAreaIndex) && (key == "" || d.DataKey == key) && d.IvData == null) .ToList() //Required to run the next Where on the C# side .Where(row => row.Expiration.GetValueOrDefault(DateTime.MaxValue) > DateTime.Now) .Select(d => new CustomDataAreaResult(d.PlusCode, d.DataKey, d.DataValue.ToUTF8String())) .ToList(); return(plusCodeData); }
public static List <Tuple <double, DbTables.Place> > GetNearbyPlacesAtDistance(string type, string plusCode, double distanceMid) { //TODO: test this out for performance. Spatial index should work for Distance but calculating it 3 times might not be very fast var db = new PraxisContext(); var geopoint = OpenLocationCode.DecodeValid(plusCode).Center; var ntsPoint = new Point(geopoint.Longitude, geopoint.Latitude); var distanceMin = distanceMid / 2; var distanceMax = distanceMid + distanceMin; List <Tuple <double, DbTables.Place> > results = db.Places .Where(o => distanceMin < o.ElementGeometry.Distance(ntsPoint) && o.ElementGeometry.Distance(ntsPoint) < distanceMax) .OrderByDescending(o => Math.Abs(distanceMid - o.ElementGeometry.Distance(ntsPoint))) .Select(o => Tuple.Create(o.ElementGeometry.Distance(ntsPoint), o)) .ToList(); return(results); }
//Allows for 1 style to pull a color from the custom data value. /// <summary> /// Creates the list of paint commands for the PlusCode cells intersecting the given area, with the given data key and style set, for the image. In this case, the color will be the tag's value. /// </summary> /// <param name="dataKey">the key to pull data from attached to any Osm Elements intersecting the area</param> /// <param name="styleSet">the style set to use when drawing the intersecting elements</param> /// <param name="stats">the info on the resulting image for calculating ops.</param> /// <returns>a list of CompletePaintOps to be passed into a DrawArea function</returns> public static List <CompletePaintOp> GetPaintOpsForCustomDataPlusCodesFromTagValue(string dataKey, string styleSet, ImageStats stats) { var db = new PraxisContext(); var poly = Converters.GeoAreaToPolygon(GeometrySupport.MakeBufferedGeoArea(stats.area)); var elements = db.AreaGameData.Where(d => d.DataKey == dataKey && poly.Intersects(d.GeoAreaIndex)).ToList(); var styles = TagParser.allStyleGroups[styleSet]; var bgOp = new CompletePaintOp(Converters.GeoAreaToPolygon(stats.area), 1, styles["background"].PaintOperations.First(), "background", 1); var pass1 = elements.Select(d => new { d.GeoAreaIndex.Area, d.GeoAreaIndex, paintOp = styles["tag"].PaintOperations, d.DataValue }); var pass2 = new List <CompletePaintOp>(elements.Count() * 2); //assuming each element has a Fill and Stroke op separately pass2.Add(bgOp); foreach (var op in pass1) { GetPaintOps(ref pass2, op.Area, op.GeoAreaIndex, op.paintOp, stats); } return(pass2); }
//Note: This should have the padding added to area before this is called, if checking for tiles that need regenerated. /// <summary> /// Checks if places existing in the DB for the given area. Will almost always return true, given admin boundaries are loaded into a database. Used for finding server boundaries and if an area needs maptiles drawn. /// </summary> /// <param name="area">The area to check for elements</param> /// <param name="source">an optional list to use instead of loading from the database.</param> /// <returns>true if any Places intersect the given GeoArea, false if not.</returns> public static bool DoPlacesExist(GeoArea area, List <DbTables.Place> source = null) { //As GetPlaces, but only checks if there are entries. var location = Converters.GeoAreaToPolygon(area); bool places; if (source == null) { var db = new PraxisContext(); places = db.Places.Any(md => md.ElementGeometry.Intersects(location)); return(places); } else { places = source.Any(md => md.ElementGeometry.Intersects(location)); } return(places); }
/// <summary> /// Creates the list of paint commands for the elements intersecting the given area, with the given data key attached to OSM elements and style set, for the image. /// </summary> /// <param name="dataKey">the key to pull data from attached to any Osm Elements intersecting the area</param> /// <param name="styleSet">the style set to use when drawing the intersecting elements</param> /// <param name="stats">the info on the resulting image for calculating ops.</param> /// <returns>a list of CompletePaintOps to be passed into a DrawArea function</returns> public static List <CompletePaintOp> GetPaintOpsForCustomDataElements(string dataKey, string styleSet, ImageStats stats) { //NOTE: this is being passed in an Area as a Geometry. The name needs clarified to show its drawing a maptile based on the gameplay data for places in that area. var db = new PraxisContext(); var poly = Converters.GeoAreaToPolygon(GeometrySupport.MakeBufferedGeoArea(stats.area)); var elements = db.PlaceGameData.Include(d => d.Place).Where(d => d.DataKey == dataKey && poly.Intersects(d.Place.ElementGeometry)).ToList(); var styles = TagParser.allStyleGroups[styleSet]; var bgOp = new CompletePaintOp(Converters.GeoAreaToPolygon(stats.area), 1, styles["background"].PaintOperations.First(), "background", 1); var pass1 = elements.Select(d => new { d.Place.AreaSize, d.Place.ElementGeometry, paintOp = styles[d.DataValue.ToUTF8String()].PaintOperations, d.DataValue }); var pass2 = new List <CompletePaintOp>(elements.Count() * 2); pass2.Add(bgOp); foreach (var op in pass1) { GetPaintOps(ref pass2, op.AreaSize, op.ElementGeometry, op.paintOp, stats); } return(pass2); }
/// <summary> /// Saves a key/value pair to the database that isn't attached to anything specific. Wil reject a pair that contains a player's device ID, PlusCode, or a map element ID. Global entries cannot be set to expire. /// </summary> /// <param name="key">The key to save to the database. Keys are unique, and you cannot have multiples of the same key.</param> /// <param name="value">The value to save with the key.</param> /// <returns>true if data was saved, false if data was not.</returns> public static bool SetGlobalData(string key, string value) { bool trackingPlayer = false; bool trackingLocation = false; var db = new PraxisContext(); if (db.PlayerData.Any(p => p.DeviceID == key || p.DeviceID == value)) { trackingPlayer = true; } if (DataCheck.IsPlusCode(key) || DataCheck.IsPlusCode(value)) { trackingLocation = true; } Guid tempCheck = new Guid(); if ((Guid.TryParse(key, out tempCheck) && db.Places.Any(osm => osm.PrivacyId == tempCheck)) || (Guid.TryParse(value, out tempCheck) && db.Places.Any(osm => osm.PrivacyId == tempCheck))) { trackingLocation = true; } if (trackingLocation && trackingPlayer) //Do not allow players and locations to be attached on the global level as a workaround to being blocked on the individual levels. { return(false); } var row = db.GlobalDataEntries.FirstOrDefault(p => p.DataKey == key); if (row == null) { row = new DbTables.GlobalDataEntries(); row.DataKey = key; db.GlobalDataEntries.Add(row); } row.DataValue = value.ToByteArrayUTF8(); return(db.SaveChanges() == 1); }
//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); }
/// <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 all gameplay tiles for a given area and saves them to the database (or files, if that option is set) /// </summary> /// <param name="areaToDraw">the GeoArea of the area to create tiles for.</param> /// <param name="saveToFiles">If true, writes to files in the output folder. If false, saves to DB.</param> public static void PregenMapTilesForArea(GeoArea areaToDraw, bool saveToFiles = false) { //There is a very similar function for this in Standalone.cs, but this one writes back to the main DB. var intersectCheck = Converters.GeoAreaToPolygon(areaToDraw); //start drawing maptiles and sorting out data. var swCorner = new OpenLocationCode(intersectCheck.EnvelopeInternal.MinY, intersectCheck.EnvelopeInternal.MinX); var neCorner = new OpenLocationCode(intersectCheck.EnvelopeInternal.MaxY, intersectCheck.EnvelopeInternal.MaxX); //declare how many map tiles will be drawn var xTiles = areaToDraw.LongitudeWidth / resolutionCell8; var yTiles = areaToDraw.LatitudeHeight / resolutionCell8; var totalTiles = Math.Truncate(xTiles * yTiles); Log.WriteLog("Starting processing maptiles for " + totalTiles + " Cell8 areas."); long mapTileCounter = 0; System.Diagnostics.Stopwatch progressTimer = new System.Diagnostics.Stopwatch(); progressTimer.Start(); //now, for every Cell8 involved, draw and name it. var yCoords = new List <double>((int)(intersectCheck.EnvelopeInternal.Height / resolutionCell8) + 1); var yVal = swCorner.Decode().SouthLatitude; while (yVal <= neCorner.Decode().NorthLatitude) { yCoords.Add(yVal); yVal += resolutionCell8; } var xCoords = new List <double>((int)(intersectCheck.EnvelopeInternal.Width / resolutionCell8) + 1); var xVal = swCorner.Decode().WestLongitude; while (xVal <= neCorner.Decode().EastLongitude) { xCoords.Add(xVal); xVal += resolutionCell8; } var allPlaces = GetPlaces(areaToDraw); object listLock = new object(); DateTime expiration = DateTime.Now.AddYears(10); foreach (var y in yCoords) { System.Diagnostics.Stopwatch thisRowSW = new System.Diagnostics.Stopwatch(); thisRowSW.Start(); var db = new PraxisContext(); db.ChangeTracker.AutoDetectChangesEnabled = false; //Make a collision box for just this row of Cell8s, and send the loop below just the list of things that might be relevant. //Add a Cell8 buffer space so all elements are loaded and drawn without needing to loop through the entire area. GeoArea thisRow = new GeoArea(y - ConstantValues.resolutionCell8, xCoords.First() - ConstantValues.resolutionCell8, y + ConstantValues.resolutionCell8 + ConstantValues.resolutionCell8, xCoords.Last() + resolutionCell8); var rowList = GetPlaces(thisRow, allPlaces); var tilesToSave = new List <MapTile>(xCoords.Count()); Parallel.ForEach(xCoords, x => { //make map tile. var plusCode = new OpenLocationCode(y, x, 10); var plusCode8 = plusCode.CodeDigits.Substring(0, 8); var plusCodeArea = OpenLocationCode.DecodeValid(plusCode8); var paddedArea = GeometrySupport.MakeBufferedGeoArea(plusCodeArea); var acheck = Converters.GeoAreaToPreparedPolygon(paddedArea); //Fastest search option is one preparedPolygon against a list of normal geometry. var areaList = rowList.Where(a => acheck.Intersects(a.ElementGeometry)).ToList(); //Get the list of areas in this maptile. int imgX = 0, imgY = 0; GetPlusCodeImagePixelSize(plusCode8, out imgX, out imgY); var info = new ImageStats(plusCodeArea, imgX, imgY); //new setup. var areaPaintOps = GetPaintOpsForStoredElements(areaList, "mapTiles", info); var tile = DrawPlusCode(plusCode8, areaPaintOps, "mapTiles"); if (saveToFiles) { File.WriteAllBytes("GameTiles\\" + plusCode8 + ".png", tile); } else { var thisTile = new MapTile() { TileData = tile, PlusCode = plusCode8, ExpireOn = expiration, AreaCovered = acheck.Geometry, StyleSet = "mapTiles" }; lock (listLock) tilesToSave.Add(thisTile); } }); mapTileCounter += xCoords.Count(); if (!saveToFiles) { db.MapTiles.AddRange(tilesToSave); db.SaveChanges(); } Log.WriteLog(mapTileCounter + " tiles processed, " + Math.Round((mapTileCounter / totalTiles) * 100, 2) + "% complete, " + Math.Round(xCoords.Count() / thisRowSW.Elapsed.TotalSeconds, 2) + " tiles per second."); }//); progressTimer.Stop(); Log.WriteLog("Area map tiles drawn in " + progressTimer.Elapsed.ToString() + ", averaged " + Math.Round(mapTileCounter / progressTimer.Elapsed.TotalSeconds, 2) + " tiles per second."); }
/// <summary> /// Call once when the server or app is started. Loads all the styles and caches baseline data for later use. /// </summary> /// <param name="onlyDefaults">if true, skip loading the styles from the DB and use Praxismapper's defaults </param> public static void Initialize(bool onlyDefaults = false, IMapTiles mapTiles = null) { MapTiles = mapTiles; List <StyleEntry> styles; if (onlyDefaults) { styles = Singletons.defaultStyleEntries; long i = 1; foreach (var s in styles) { foreach (var p in s.PaintOperations) { p.Id = i++; } } } else { try { //Load TPE entries from DB for app. var db = new PraxisContext(); styles = db.StyleEntries.Include(t => t.StyleMatchRules).Include(t => t.PaintOperations).ToList(); if (styles == null || styles.Count() == 0) { styles = Singletons.defaultStyleEntries; } var bitmaps = db.StyleBitmaps.ToList(); foreach (var b in bitmaps) { cachedBitmaps.Add(b.Filename, b.Data); //Actual MapTiles dll will process the bitmap, we just load it here. } } catch (Exception ex) { //The database doesn't exist, use defaults. Log.WriteLog("Error initializing:" + ex.Message, Log.VerbosityLevels.Errors); styles = Singletons.defaultStyleEntries; } } var groups = styles.GroupBy(s => s.StyleSet); foreach (var g in groups) { allStyleGroups.Add(g.Key, g.Select(gg => gg).OrderBy(gg => gg.MatchOrder).ToDictionary(k => k.Name, v => v)); } //Default style should be transparent, matches anything (assumed other styles were checked first) defaultStyle = new StyleEntry() { MatchOrder = 10000, Name = "unmatched", StyleSet = "mapTiles", PaintOperations = new List <StylePaint>() { new StylePaint() { HtmlColorCode = "00000000", FillOrStroke = "fill", LineWidthDegrees = 1, LinePattern = "solid", LayerId = 101 } }, StyleMatchRules = new List <StyleMatchRule>() { new StyleMatchRule() { Key = "*", Value = "*", MatchType = "default" } } }; MapTiles.Initialize(); }
/// <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); }