public ActionResult GetPlaceInfo(long sourceElementId, int sourceElementType)
        {
            var db   = new PraxisContext();
            var area = db.Places.Include(e => e.Tags).FirstOrDefault(e => e.SourceItemID == sourceElementId && e.SourceItemType == sourceElementType);

            if (area == null)
            {
                return(View());
            }

            TagParser.ApplyTags(new System.Collections.Generic.List <DbTables.Place>()
            {
                area
            }, "mapTiles");
            ViewBag.areaname = TagParser.GetPlaceName(area.Tags);
            ViewBag.type     = area.GameElementName;

            var geoarea = Converters.GeometryToGeoArea(area.ElementGeometry.Envelope);

            geoarea = new Google.OpenLocationCode.GeoArea(geoarea.SouthLatitude - ConstantValues.resolutionCell10,
                                                          geoarea.WestLongitude - ConstantValues.resolutionCell10,
                                                          geoarea.NorthLatitude + ConstantValues.resolutionCell10,
                                                          geoarea.EastLongitude + ConstantValues.resolutionCell10); //add some padding to the edges.
            ImageStats istats = new ImageStats(geoarea, (int)(geoarea.LongitudeWidth / ConstantValues.resolutionCell11Lon), (int)(geoarea.LatitudeHeight / ConstantValues.resolutionCell11Lat));

            //sanity check: we don't want to draw stuff that won't fit in memory, so check for size and cap it if needed
            if (istats.imageSizeX * istats.imageSizeY > 8000000)
            {
                var ratio   = geoarea.LongitudeWidth / geoarea.LatitudeHeight; //W:H,
                var newSize = (istats.imageSizeY > 2000 ? 2000 : istats.imageSizeY);
                istats = new ImageStats(geoarea, (int)(newSize * ratio), newSize);
            }
            System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch();
            sw.Start();
            //var tileSvg = MapTiles.DrawAreaAtSizeSVG(istats); ViewBag.UseSvg = true;
            var tile = MapTiles.DrawAreaAtSize(istats); ViewBag.UseSvg = false;

            sw.Stop();

            ViewBag.imageString = "data:image/png;base64," + Convert.ToBase64String(tile);
            //ViewBag.imageString = tileSvg.Substring(39); //skip the <xml> tag
            ViewBag.timeToDraw  = sw.Elapsed;
            ViewBag.placeCount  = 0;
            ViewBag.areasByType = "";
            var places = Place.GetPlaces(istats.area);

            ViewBag.placeCount = places.Count();
            var    grouped     = places.GroupBy(p => p.GameElementName);
            string areasByType = "";

            foreach (var g in grouped)
            {
                areasByType += g.Key + g.Count() + "<br />";
            }

            ViewBag.areasByType = areasByType;

            return(View());
        }
        /// <summary>
        /// Convert main DB OSM elements into compact estimated offline DB place info records.
        /// </summary>
        /// <param name="allPlaces">the list of elements to copy into the standalone DB</param>
        /// <returns>a list of PlaceInfo items to use in the standalone DB</returns>
        public static List <PlaceInfo2> GetPlaceInfo(List <DbTables.Place> allPlaces)
        {
            var results = new List <PlaceInfo2>();

            foreach (var place in allPlaces) //.Where(p => p.IsGameElement))
            {
                var center = place.ElementGeometry.Centroid.Coordinate;
                //The less symmetrical an area's envelope is, the less accurate this guess is. But that's the tradeoff i'm making
                //to get this all self-contained in the least amount of space. / 4 because (/2 for average, then /2 for radius instead of diameter)
                //Circular radius was replaced with square envelope. it's 1 extra double to store per row to do the envelope check this way, and looks more reasonable.
                //var calcRadius = (place.elementGeometry.EnvelopeInternal.Width + place.elementGeometry.EnvelopeInternal.Height) / 4;
                var pi = new PlaceInfo2()
                {
                    Name      = TagParser.GetPlaceName(place.Tags),
                    areaType  = place.GameElementName,
                    latCenter = center.Y,
                    lonCenter = center.X,
                    height    = place.ElementGeometry.EnvelopeInternal.Height,
                    width     = place.ElementGeometry.EnvelopeInternal.Width,
                    PlaceId   = place.SourceItemID
                };

                //Make points 1 Cell10 in size, so they're detectable.
                if (pi.height == 0)
                {
                    pi.height = .000125;
                }
                if (pi.width == 0)
                {
                    pi.width = .000125;
                }
                results.Add(pi);
            }

            return(results);
        }
        //These functions are for taking a PraxisMapper DB and making a workable mobile version. Much less accurate, requires 0 data connectivity.
        //No longer a high priority but I do want to keep this code around.

        public static void CreateStandaloneDB(long relationID = 0, GeoArea bounds = null, bool saveToDB = false, bool saveToFolder = true)
        {
            //TODO: could rename TerrainInfo to TrailInfo, terrainDataSmall to trailData

            string name = "";

            if (bounds != null)
            {
                name = Math.Truncate(bounds.SouthLatitude) + "_" + Math.Truncate(bounds.WestLongitude) + "_" + Math.Truncate(bounds.NorthLatitude) + "_" + Math.Truncate(bounds.EastLongitude) + ".sqlite";
            }

            if (relationID > 0)
            {
                name = relationID.ToString() + ".sqlite";
            }

            if (File.Exists(name))
            {
                File.Delete(name);
            }

            var mainDb   = new PraxisContext();
            var sqliteDb = new StandaloneContext(relationID.ToString());

            sqliteDb.ChangeTracker.AutoDetectChangesEnabled = false;
            sqliteDb.Database.EnsureCreated();
            Log.WriteLog("Standalone DB created for relation " + relationID + " at " + DateTime.Now);

            GeoArea buffered;

            if (relationID > 0)
            {
                var fullArea = mainDb.Places.FirstOrDefault(m => m.SourceItemID == relationID && m.SourceItemType == 3);
                if (fullArea == null)
                {
                    return;
                }

                buffered = Converters.GeometryToGeoArea(fullArea.ElementGeometry);
                //This should also be able to take a bounding box in addition in the future.
            }
            else
            {
                buffered = bounds;
            }

            //TODO: set a flag to allow this to pull straight from a PBF file?
            List <DbTables.Place> allPlaces = new List <DbTables.Place>();
            var  intersectCheck             = Converters.GeoAreaToPolygon(buffered);
            bool pullFromPbf = false; //Set via arg at startup? or setting file?

            if (!pullFromPbf)
            {
                allPlaces = PraxisCore.Place.GetPlaces(buffered);
            }
            else
            {
                //need a file to read from.
                //optionally a bounding box on that file.
                //Starting to think i might want to track some generic parameters I refer to later. like -box|s|w|n|e or -point|lat|long or -singleFile|here.osm.pbf
                //allPlaces = PbfFileParser.ProcessSkipDatabase();
            }

            Log.WriteLog("Loaded all intersecting geometry at " + DateTime.Now);

            string minCode          = new OpenLocationCode(buffered.SouthLatitude, buffered.WestLongitude).CodeDigits;
            string maxCode          = new OpenLocationCode(buffered.NorthLatitude, buffered.EastLongitude).CodeDigits;
            int    removableLetters = 0;

            for (int i = 0; i < 10; i++)
            {
                if (minCode[i] == maxCode[i])
                {
                    removableLetters++;
                }
                else
                {
                    i += 10;
                }
            }
            string commonStart = minCode.Substring(0, removableLetters);

            var wikiList = allPlaces.Where(a => a.Tags.Any(t => t.Key == "wikipedia") && TagParser.GetPlaceName(a.Tags) != "").Select(a => TagParser.GetPlaceName(a.Tags)).Distinct().ToList();
            //Leaving this nearly wide open, since it's not the main driver of DB size.
            var basePlaces    = allPlaces.Where(a => TagParser.GetPlaceName(a.Tags) != "" || a.GameElementName != "unmatched").ToList(); //.Where(a => a.name != "").ToList();// && (a.IsGameElement || wikiList.Contains(a.name))).ToList();
            var distinctNames = basePlaces.Select(p => TagParser.GetPlaceName(p.Tags)).Distinct().ToList();                              //This distinct might be causing things in multiple pieces to only detect one of them, not all of them?

            var placeInfo = PraxisCore.Standalone.Standalone.GetPlaceInfo(basePlaces);
            //Remove trails later.
            //SHORTCUT: for roads that are a straight-enough line (under 1 Cell10 in width or height)
            //just treat them as being 1 Cell10 in that axis, and skip tracking them by each Cell10 they cover.
            HashSet <long> skipEntries = new HashSet <long>();

            foreach (var pi in placeInfo.Where(p => p.areaType == "road" || p.areaType == "trail"))
            {
                //If a road is nearly a straight line, treat it as though it was 1 cell10 wide, and don't index its coverage per-cell later.
                if (pi.height <= ConstantValues.resolutionCell10 && pi.width >= ConstantValues.resolutionCell10)
                {
                    pi.height = ConstantValues.resolutionCell10; skipEntries.Add(pi.PlaceId);
                }
                else if (pi.height >= ConstantValues.resolutionCell10 && pi.width <= ConstantValues.resolutionCell10)
                {
                    pi.width = ConstantValues.resolutionCell10; skipEntries.Add(pi.PlaceId);
                }
            }

            sqliteDb.PlaceInfo2s.AddRange(placeInfo);
            sqliteDb.SaveChanges();
            Log.WriteLog("Processed geometry at " + DateTime.Now);
            var placeDictionary = placeInfo.ToDictionary(k => k.PlaceId, v => v);

            //to save time, i need to index which areas are in which Cell6.
            //So i know which entries I can skip when running.
            var indexCell6 = PraxisCore.Standalone.Standalone.IndexAreasPerCell6(buffered, basePlaces);
            var indexes    = indexCell6.SelectMany(i => i.Value.Select(v => new PlaceIndex()
            {
                PlusCode = i.Key, placeInfoId = placeDictionary[v.SourceItemID].id
            })).ToList();

            sqliteDb.PlaceIndexs.AddRange(indexes);

            sqliteDb.SaveChanges();
            Log.WriteLog("Processed Cell6 index table at " + DateTime.Now);

            //trails need processed the old way, per Cell10, when they're not simply a straight-line.
            //Roads too.
            var tdSmalls = new Dictionary <string, TerrainDataSmall>(); //Possible issue: a trail and a road with the same name would only show up as whichever one got in the DB first.
            var toRemove = new List <PlaceInfo2>();

            foreach (var trail in basePlaces.Where(p => (p.GameElementName == "trail" || p.GameElementName == "road"))) //TODO: add rivers here?
            {
                if (skipEntries.Contains(trail.SourceItemID))
                {
                    continue; //Don't per-cell index this one, we shifted it's envelope to handle it instead.
                }
                if (TagParser.GetPlaceName(trail.Tags) == "")
                {
                    continue; //So sorry, but there's too damn many roads without names inflating DB size without being useful as-is.
                }
                var p = placeDictionary[trail.SourceItemID];
                toRemove.Add(p);

                GeoArea thisPath = Converters.GeometryToGeoArea(trail.ElementGeometry);
                List <DbTables.Place> oneEntry = new List <DbTables.Place>();
                oneEntry.Add(trail);

                var overlapped = AreaTypeInfo.SearchArea(ref thisPath, ref oneEntry);
                if (overlapped.Count() > 0)
                {
                    tdSmalls.TryAdd(TagParser.GetPlaceName(trail.Tags), new TerrainDataSmall()
                    {
                        Name = TagParser.GetPlaceName(trail.Tags), areaType = trail.GameElementName
                    });
                }
                foreach (var o in overlapped)
                {
                    var ti = new TerrainInfo();
                    ti.PlusCode         = o.Key.Substring(removableLetters, 10 - removableLetters);
                    ti.TerrainDataSmall = new List <TerrainDataSmall>();
                    ti.TerrainDataSmall.Add(tdSmalls[o.Value.Name]);
                    sqliteDb.TerrainInfo.Add(ti);
                }
                sqliteDb.SaveChanges();
            }

            foreach (var r in toRemove.Distinct())
            {
                sqliteDb.PlaceInfo2s.Remove(r);
            }
            sqliteDb.SaveChanges();
            Log.WriteLog("Trails processed at " + DateTime.Now);

            //make scavenger hunts
            var sh = PraxisCore.Standalone.Standalone.GetScavengerHunts(allPlaces);

            sqliteDb.ScavengerHunts.AddRange(sh);
            sqliteDb.SaveChanges();
            Log.WriteLog("Auto-created scavenger hunt entries at " + DateTime.Now);

            var swCorner = new OpenLocationCode(intersectCheck.EnvelopeInternal.MinY, intersectCheck.EnvelopeInternal.MinX);
            var neCorner = new OpenLocationCode(intersectCheck.EnvelopeInternal.MaxY, intersectCheck.EnvelopeInternal.MaxX);

            //insert default entries for a new player.
            sqliteDb.PlayerStats.Add(new PlayerStats()
            {
                timePlayed = 0, distanceWalked = 0, score = 0
            });
            sqliteDb.Bounds.Add(new Bounds()
            {
                EastBound = neCorner.Decode().EastLongitude, NorthBound = neCorner.Decode().NorthLatitude, SouthBound = swCorner.Decode().SouthLatitude, WestBound = swCorner.Decode().WestLongitude, commonCodeLetters = commonStart, BestIdleCompletionTime = 0, LastPlayedOn = 0, StartedCurrentIdleRun = 0
            });
            sqliteDb.IdleStats.Add(new IdleStats()
            {
                emptySpacePerSecond = 0, emptySpaceTotal = 0, graveyardSpacePerSecond = 0, graveyardSpaceTotal = 0, natureReserveSpacePerSecond = 0, natureReserveSpaceTotal = 0, parkSpacePerSecond = 0, parkSpaceTotal = 0, touristSpacePerSecond = 0, touristSpaceTotal = 0, trailSpacePerSecond = 0, trailSpaceTotal = 0
            });
            sqliteDb.SaveChanges();

            //now we have the list of places we need to be concerned with.
            System.IO.Directory.CreateDirectory(relationID + "Tiles");
            PraxisCore.Standalone.Standalone.DrawMapTilesStandalone(relationID, buffered, allPlaces, saveToFolder);
            sqliteDb.SaveChanges();
            Log.WriteLog("Maptiles drawn at " + DateTime.Now);

            //Copy the files as necessary to their correct location.
            //if (saveToFolder)
            //Directory.Move(relationID + "Tiles", config["Solar2dExportFolder"] + "Tiles");

            //File.Copy(relationID + ".sqlite", config["Solar2dExportFolder"] + "database.sqlite");

            Log.WriteLog("Standalone gameplay DB done.");
        }
        /// <summary>
        /// Auto-create some scavenger hunt entries for a standalone game DB. Makes a list for entries that have a wikipedia tag,
        /// and then entries for all styles in the first TagParser style that have IsGameElement set to true.
        /// </summary>
        /// <param name="allPlaces">the list of OSM elements to search</param>
        /// <returns>the list of scavenger hunt entries</returns>
        public static List <ScavengerHuntStandalone> GetScavengerHunts(List <DbTables.Place> allPlaces)
        {
            //TODO: This is going to need updated for the new TagParser rules. testing at the minimum
            var results  = new List <ScavengerHuntStandalone>();
            var wikiList = allPlaces.Where(a => a.Tags.Any(t => t.Key == "wikipedia") && TagParser.GetPlaceName(a.Tags) != "").Select(a => TagParser.GetPlaceName(a.Tags)).Distinct().ToList();
            //Create automatic scavenger hunt entries.
            Dictionary <string, List <string> > scavengerHunts = new Dictionary <string, List <string> >();

            //NOTE:
            //If i run this by elementID, i get everything unique but several entries get duplicated becaues they're in multiple pieces.
            //If I run this by name, the lists are much shorter but visiting one distinct location might count for all of them (This is a bigger concern with very large areas or retail establishment)
            //So I'm going to run this by name for the player's sake.
            scavengerHunts.Add("Wikipedia Places", wikiList);
            Log.WriteLog(wikiList.Count() + " Wikipedia-linked items found for scavenger hunt.");
            //fill in gameElement lists.
            foreach (var gameElementTags in TagParser.allStyleGroups.First().Value.Where(s => s.Value.IsGameElement))
            {
                var foundElements = allPlaces.Where(a => TagParser.MatchOnTags(gameElementTags.Value, a.Tags) && !string.IsNullOrWhiteSpace(TagParser.GetPlaceName(a.Tags))).Select(a => TagParser.GetPlaceName(a.Tags)).Distinct().ToList();
                scavengerHunts.Add(gameElementTags.Value.Name, foundElements);
                Log.WriteLog(foundElements.Count() + " " + gameElementTags.Value.Name + " items found for scavenger hunt.");
            }

            foreach (var hunt in scavengerHunts)
            {
                foreach (var item in hunt.Value)
                {
                    results.Add(new ScavengerHuntStandalone()
                    {
                        listName = hunt.Key, description = item, playerHasVisited = false
                    });
                }
            }
            return(results);
        }