[Route("/[controller]/AreaPlaceData/{code}/{styleSet}/{dataKey}")] //Draw an area using place data. public ActionResult DrawPlusCodeCustomElements(string code, string styleSet, string dataKey) { try { PerformanceTracker pt = new PerformanceTracker("DrawTilePlace"); MapTileSupport.GetPlusCodeImagePixelSize(code, out var imgX, out var imgY); var info = new ImageStats(OpenLocationCode.DecodeValid(code), imgX, imgY); if (!DataCheck.IsInBounds(cache.Get <IPreparedGeometry>("serverBounds"), info.area)) { pt.Stop("OOB"); return(StatusCode(500)); } byte[] tileData = getExistingSlippyTile(code, styleSet); if (tileData != null) { pt.Stop(code + "|" + styleSet); return(File(tileData, "image/png")); } //Make tile var places = GetPlacesForTile(info, null, styleSet, false); var paintOps = MapTileSupport.GetPaintOpsForCustomDataElements(dataKey, styleSet, info); tileData = FinishMapTile(info, paintOps, code, styleSet); pt.Stop(code + "|" + styleSet + "|" + Configuration.GetValue <string>("MapTilesEngine")); return(File(tileData, "image/png")); } catch (Exception ex) { ErrorLogger.LogError(ex); return(StatusCode(500)); } }
/// <summary> /// Create the map tiles for an offline game, to be stored on the device alongside the database. /// </summary> /// <param name="relationID">the OSM relation the game will be built around</param> /// <param name="buffered">the GeoArea to use as the gameplay area</param> /// <param name="allPlaces">List of OSM Elements to use while drawing tiles</param> /// <param name="saveToFolder">If true, save images to a named folder. Does not currently save any output if this is false.</param> public static void DrawMapTilesStandalone(long relationID, GeoArea buffered, List <DbTables.Place> allPlaces, bool saveToFolder) { var intersectCheck = Converters.GeoAreaToPolygon(buffered); //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 = buffered.LongitudeWidth / resolutionCell8; var yTiles = buffered.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. //This is tricky to run in parallel because it's not smooth increments var yCoords = new List <double>(); var yVal = swCorner.Decode().SouthLatitude; while (yVal <= neCorner.Decode().NorthLatitude) { yCoords.Add(yVal); yVal += resolutionCell8; } var xCoords = new List <double>(); var xVal = swCorner.Decode().WestLongitude; while (xVal <= neCorner.Decode().EastLongitude) { xCoords.Add(xVal); xVal += resolutionCell8; } System.Threading.ReaderWriterLockSlim dbLock = new System.Threading.ReaderWriterLockSlim(); foreach (var y in yCoords) { //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 row = Converters.GeoAreaToPolygon(thisRow); var rowList = allPlaces.Where(a => row.Intersects(a.ElementGeometry)).ToList(); Parallel.ForEach(xCoords, x => //foreach (var x in xCoords) { //make map tile. var plusCode = new OpenLocationCode(y, x, 10); var plusCode8 = plusCode.CodeDigits.Substring(0, 8); var plusCodeArea = OpenLocationCode.DecodeValid(plusCode8); var areaForTile = new GeoArea(new GeoPoint(plusCodeArea.SouthLatitude, plusCodeArea.WestLongitude), new GeoPoint(plusCodeArea.NorthLatitude, plusCodeArea.EastLongitude)); var acheck = Converters.GeoAreaToPolygon(areaForTile); //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 //Create the maptile first, so if we save it to the DB/a file we can call the lock once per loop. int imgX = 0, imgY = 0; MapTileSupport.GetPlusCodeImagePixelSize(plusCode8, out imgX, out imgY); var info = new ImageStats(areaForTile, imgX, imgY); //Each pixel is a Cell11, we're drawing a Cell8. For Cell6 testing this is 1600x2000, just barely within android limits byte[] tile = null; // MapTiles.DrawAreaAtSize(info, areaList); if (tile == null) { Log.WriteLog("Tile at " + x + "," + y + "Failed to draw!"); return; } if (saveToFolder) //some apps, like my Solar2D apps, can't use the byte[] in a DB row and need files. { //This split helps (but does not alleviate) Solar2D performance. //A county-sized app will function this way, though sometimes when zoomed out it will not load all map tiles on an android device. Directory.CreateDirectory(relationID + "Tiles\\" + plusCode8.Substring(0, 6)); System.IO.File.WriteAllBytes(relationID + "Tiles\\" + plusCode8.Substring(0, 6) + "\\" + plusCode8.Substring(6, 2) + ".pngTile", tile); //Solar2d also can't load pngs directly from an apk file in android, but the rule is extension based. } mapTileCounter++; //if (progressTimer.ElapsedMilliseconds > 15000) //{ // progressTimer.Restart(); //} }); Log.WriteLog(mapTileCounter + " tiles processed, " + Math.Round((mapTileCounter / totalTiles) * 100, 2) + "% complete"); }//); }