/// <summary>
        /// If a location map pixel is on a gradient greater than threshold, then
        /// smooth surrounding Moore neighbourhood with location height
        /// </summary>
        public static void SmoothLocationNeighbourhood(ContentReader contentReader, int threshold = 20)
        {
            //System.Diagnostics.Stopwatch stopwatch = System.Diagnostics.Stopwatch.StartNew();
            //long startTime = stopwatch.ElapsedMilliseconds;

            // Get in-memory height array
            byte[] heightArray = contentReader.WoodsFileReader.Buffer;

            // Search for locations
            for (int y = 1; y < WoodsFile.mapHeightValue - 1; y++)
            {
                for (int x = 1; x < WoodsFile.mapWidthValue - 1; x++)
                {
                    ContentReader.MapSummary summary;
                    if (contentReader.HasLocation(x, y, out summary))
                    {
                        // Use Sobel filter for gradient
                        float x0y0 = heightArray[y * WoodsFile.mapWidthValue + x];
                        float x1y0 = heightArray[y * WoodsFile.mapWidthValue + (x + 1)];
                        float x0y1 = heightArray[(y + 1) * WoodsFile.mapWidthValue + x];
                        float gradient = GetGradient(x0y0, x1y0, x0y1);
                        if (gradient > threshold)
                        {
                            AverageHeights(ref heightArray, x, y);
                        }
                    }
                }
            }

            //long totalTime = stopwatch.ElapsedMilliseconds - startTime;
            //DaggerfallUnity.LogMessage(string.Format("Time to smooth location neighbourhoods: {0}ms", totalTime), true);
        }
        /// <summary>
        /// initializes resources (mapDistanceSquaredFromWater, mapDistanceSquaredFromLocations, mapMultipliers) and smoothes small height map
        /// </summary>
        public static void InitImprovedWorldTerrain(ContentReader contentReader)
        {
            if (!init)
            {
                #if CREATE_PERSISTENT_LOCATION_RANGE_MAPS
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;

                    mapLocationRangeX = new byte[width * height];
                    mapLocationRangeY = new byte[width * height];

                    //int y = 204;
                    //int x = 718;
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            //MapPixelData MapData = TerrainHelper.GetMapPixelData(contentReader, x, y);
                            //if (MapData.hasLocation)
                            //{
                            //    int locationRangeX = (int)MapData.locationRect.xMax - (int)MapData.locationRect.xMin;
                            //    int locationRangeY = (int)MapData.locationRect.yMax - (int)MapData.locationRect.yMin;
                            //}

                            ContentReader.MapSummary mapSummary;
                            int regionIndex = -1, mapIndex = -1;
                            bool hasLocation = contentReader.HasLocation(x, y, out mapSummary);
                            if (hasLocation)
                            {   
                                regionIndex = mapSummary.RegionIndex;
                                mapIndex = mapSummary.MapIndex;
                                DFLocation location = contentReader.MapFileReader.GetLocation(regionIndex, mapIndex);
                                byte locationRangeX = location.Exterior.ExteriorData.Width;
                                byte locationRangeY = location.Exterior.ExteriorData.Height;

                                mapLocationRangeX[y * width + x] = locationRangeX;
                                mapLocationRangeY[y * width + x] = locationRangeY;
                            }
                        }
                    }
              
                    // save to files
                    FileStream ostream;
                    ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathMapLocationRangeX), FileMode.Create, FileAccess.Write);
                    BinaryWriter writerMapLocationRangeX = new BinaryWriter(ostream, Encoding.UTF8);
                    writerMapLocationRangeX.Write(mapLocationRangeX, 0, width * height);
                    writerMapLocationRangeX.Close();
                    ostream.Close();

                    ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathMapLocationRangeY), FileMode.Create, FileAccess.Write);
                    BinaryWriter writerMapLocationRangeY = new BinaryWriter(ostream, Encoding.UTF8);
                    writerMapLocationRangeY.Write(mapLocationRangeY, 0, width * height);
                    writerMapLocationRangeY.Close();
                    ostream.Close();
                }
                #else
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;

                    mapLocationRangeX = new byte[width * height];
                    mapLocationRangeY = new byte[width * height];

                    MemoryStream istream;
                    TextAsset assetMapLocationRangeX = Resources.Load<TextAsset>(filenameMapLocationRangeX);
                    if (assetMapLocationRangeX != null)
                    {
                        istream = new MemoryStream(assetMapLocationRangeX.bytes);
                        BinaryReader readerMapLocationRangeX = new BinaryReader(istream, Encoding.UTF8);
                        readerMapLocationRangeX.Read(mapLocationRangeX, 0, width * height);
                        readerMapLocationRangeX.Close();
                        istream.Close();
                    }

                    TextAsset assetMapLocationRangeY = Resources.Load<TextAsset>(filenameMapLocationRangeY);
                    if (assetMapLocationRangeY)
                    {
                        istream = new MemoryStream(assetMapLocationRangeY.bytes);
                        BinaryReader readerMapLocationRangeY = new BinaryReader(istream, Encoding.UTF8);
                        readerMapLocationRangeY.Read(mapLocationRangeY, 0, width * height);
                        readerMapLocationRangeY.Close();
                        istream.Close();
                    }

                    //FileStream istream;
                    //istream = new FileStream(filepathMapLocationRangeX, FileMode.Open, FileAccess.Read);
                    //BinaryReader readerMapLocationRangeX = new BinaryReader(istream, Encoding.UTF8);
                    //readerMapLocationRangeX.Read(mapLocationRangeX, 0, width * height);
                    //readerMapLocationRangeX.Close();
                    //istream.Close();

                    //istream = new FileStream(filepathMapLocationRangeY, FileMode.Open, FileAccess.Read);
                    //BinaryReader readerMapLocationRangeY = new BinaryReader(istream, Encoding.UTF8);
                    //readerMapLocationRangeY.Read(mapLocationRangeY, 0, width * height);
                    //readerMapLocationRangeY.Close();
                    //istream.Close();
                }
                #endif

                if (mapDistanceSquaredFromWater == null)
                {
                    byte[] heightMapArray = contentReader.WoodsFileReader.Buffer.Clone() as byte[];
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            if (heightMapArray[y * width + x] <= 2)
                                heightMapArray[y * width + x] = 1;
                            else
                                heightMapArray[y * width + x] = 0;
                        }
                    }
                    //now set image borders to "water" (this is a workaround to prevent mountains to become too high in north-east and south-east edge of map)
                    for (int y = 0; y < height; y++)
                    {
                        heightMapArray[y * width + 0] = 1;
                        heightMapArray[y * width + width - 1] = 1;
                    }
                    for (int x = 0; x < width; x++)
                    {
                        heightMapArray[0 * width + x] = 1;
                        heightMapArray[(height - 1) * width + x] = 1;
                    }

                    mapDistanceSquaredFromWater = imageDistanceTransform(heightMapArray, width, height, 1);

                    heightMapArray = null;
                }

                if (mapDistanceSquaredFromLocations == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapLocations = new byte[width * height];

                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            ContentReader.MapSummary summary;
                            if (contentReader.HasLocation(x + 1, height - 1 - y, out summary))
                                mapLocations[y * width + x] = 1;
                            else
                                mapLocations[y * width + x] = 0;
                        }
                    }
                    mapDistanceSquaredFromLocations = imageDistanceTransform(mapLocations, width, height, 1);
                }



                if (mapMultipliers == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapMultipliers = new float[width * height];

                    // compute the multiplier and store it in mapMultipliers
                    for (int y = 0; y < height; y++)
                    {
                        for (int x = 0; x < width; x++)
                        {
                            float distanceFromWater = (float)Math.Sqrt(mapDistanceSquaredFromWater[y * WoodsFile.mapWidthValue + x]);
                            float distanceFromLocation = (float)Math.Sqrt(mapDistanceSquaredFromLocations[y * WoodsFile.mapWidthValue + x]);
                            float multiplierLocation = (distanceFromLocation * extraExaggerationFactorLocationDistance + 1.0f); // terrain distant from location gets extra exaggeration
                            if (distanceFromWater < minDistanceFromWaterForExtraExaggeration) // except if it is near water
                                multiplierLocation = 1.0f;
                            mapMultipliers[y * width + x] = (Math.Min(maxHeightsExaggerationMultiplier, multiplierLocation * Math.Max(1.0f, distanceFromWater * exaggerationFactorWaterDistance)));
                        }
                    }

                    // multipliedMap gets smoothed
                    float[] newmapMultipliers = mapMultipliers.Clone() as float[];
                    float[,] weights = { { 0.0625f, 0.125f, 0.0625f }, { 0.125f, 0.25f, 0.125f }, { 0.0625f, 0.125f, 0.0625f } };
                    for (int y = 1; y < height - 1; y++)
                    {
                        for (int x = 1; x < width - 1; x++)
                        {
                            if (mapDistanceSquaredFromLocations[y * width + x] <= 2) // at and around locations ( <= 2 ... only map pixels in 8-connected neighborhood (distanceFromLocationMaps stores squared distances...))
                            {
                                newmapMultipliers[y * width + x] =
                                    weights[0, 0] * mapMultipliers[(y - 1) * width + (x - 1)] + weights[0, 1] * mapMultipliers[(y - 1) * width + (x)] + weights[0, 2] * mapMultipliers[(y - 1) * width + (x + 1)] +
                                    weights[1, 0] * mapMultipliers[(y - 0) * width + (x - 1)] + weights[1, 1] * mapMultipliers[(y - 0) * width + (x)] + weights[1, 2] * mapMultipliers[(y - 0) * width + (x + 1)] +
                                    weights[2, 0] * mapMultipliers[(y + 1) * width + (x - 1)] + weights[2, 1] * mapMultipliers[(y + 1) * width + (x)] + weights[2, 2] * mapMultipliers[(y + 1) * width + (x + 1)];
                            }
                        }
                    }
                    mapMultipliers = newmapMultipliers;

                    newmapMultipliers = null;
                    weights = null;
                }

                //the height map gets smoothed as well
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    byte[] heightMapBuffer = contentReader.WoodsFileReader.Buffer.Clone() as byte[];
                    int[,] intWeights = { { 1, 2, 1 }, { 2, 4, 2 }, { 1, 2, 1 } };
                    for (int y = 1; y < height - 1; y++)
                    {
                        for (int x = 1; x < width - 1; x++)
                        {
                            if (mapDistanceSquaredFromWater[y * width + x] > 0) // check if squared distance from water is greater than zero -> if it is no water pixel
                            {
                                int value =
                                    intWeights[0, 0] * (int)heightMapBuffer[(y - 1) * width + (x - 1)] + intWeights[0, 1] * (int)heightMapBuffer[(y - 1) * width + (x)] + intWeights[0, 2] * (int)heightMapBuffer[(y - 1) * width + (x + 1)] +
                                    intWeights[1, 0] * (int)heightMapBuffer[(y - 0) * width + (x - 1)] + intWeights[1, 1] * (int)heightMapBuffer[(y - 0) * width + (x)] + intWeights[1, 2] * (int)heightMapBuffer[(y - 0) * width + (x + 1)] +
                                    intWeights[2, 0] * (int)heightMapBuffer[(y + 1) * width + (x - 1)] + intWeights[2, 1] * (int)heightMapBuffer[(y + 1) * width + (x)] + intWeights[2, 2] * (int)heightMapBuffer[(y + 1) * width + (x + 1)];

                                heightMapBuffer[y * width + x] = (byte)(value / 16);
                            }
                        }
                    }
                    contentReader.WoodsFileReader.Buffer = heightMapBuffer;

                    heightMapBuffer = null;
                    intWeights = null;
                }

                // build tree coverage map
                if (mapTreeCoverage == null)
                {
                    int width = WoodsFile.mapWidthValue;
                    int height = WoodsFile.mapHeightValue;
                    mapTreeCoverage = new byte[width * height];

                    #if !LOAD_TREE_COVERAGE_MAP
                    {
                        float startTreeCoverageAtElevation = ImprovedTerrainSampler.baseHeightScale * 2.0f; // ImprovedTerrainSampler.scaledBeachElevation;
                        float minTreeCoverageSaturated = ImprovedTerrainSampler.baseHeightScale * 6.0f;
                        float maxTreeCoverageSaturated = ImprovedTerrainSampler.baseHeightScale * 60.0f;
                        float endTreeCoverageAtElevation = ImprovedTerrainSampler.baseHeightScale * 80.0f;
                        //float maxElevation = 0.0f;
                        for (int y = 0; y < height; y++)
                        {
                            for (int x = 0; x < width; x++)
                            {
                                int readIndex = (height - 1 - y) * width + x;
                                float w = 0.0f;

                                //float elevation = ((float)contentReader.WoodsFileReader.Buffer[(height - 1 - y) * width + x]) / 255.0f; // *mapMultipliers[index];
                                float elevation = ((float)contentReader.WoodsFileReader.Buffer[readIndex]) * mapMultipliers[readIndex];

                                //maxElevation = Math.Max(maxElevation, elevation);
                                if ((elevation > minTreeCoverageSaturated) && (elevation < maxTreeCoverageSaturated))
                                {
                                    w = 1.0f;
                                }
                                else if ((elevation >= startTreeCoverageAtElevation) && (elevation <= minTreeCoverageSaturated))
                                {
                                    w = (elevation - startTreeCoverageAtElevation) / (minTreeCoverageSaturated - startTreeCoverageAtElevation);
                                }
                                else if ((elevation >= maxTreeCoverageSaturated) && (elevation <= endTreeCoverageAtElevation))
                                {
                                    w = 1.0f - ((elevation - maxTreeCoverageSaturated) / (endTreeCoverageAtElevation - maxTreeCoverageSaturated));
                                }

                                //w = 0.65f * w + 0.35f * Math.Min(6.0f, (float)Math.Sqrt(mapDistanceSquaredFromLocations[y * width + x])) / 6.0f;

                                mapTreeCoverage[(y) * width + x] = Convert.ToByte(w * 255.0f);

                                //if (elevation>0.05f)
                                //    mapTreeCoverage[index] = Convert.ToByte(250); //w * 255.0f);
                                //else mapTreeCoverage[index] = Convert.ToByte(0);

                                //if (elevation >= startTreeCoverageAtElevation)
                                //{
                                //    mapTreeCoverage[(y) * width + x] = Convert.ToByte(255.0f);
                                //} else{
                                //    mapTreeCoverage[(y) * width + x] = Convert.ToByte(0.0f);
                                //}
                            }
                        }
                    }
                    #else
                    {
                        MemoryStream istream;
                        TextAsset assetMapTreeCoverage = Resources.Load<TextAsset>(filenameTreeCoverageMap);
                        if (assetMapTreeCoverage)
                        {
                            istream = new MemoryStream(assetMapTreeCoverage.bytes);
                            BinaryReader readerMapTreeCoverage = new BinaryReader(istream, Encoding.UTF8);
                            readerMapTreeCoverage.Read(mapTreeCoverage, 0, width * height);
                            readerMapTreeCoverage.Close();
                            istream.Close();
                        }
                    }
                    #endif

                    #if CREATE_PERSISTENT_TREE_COVERAGE_MAP
                    {
                        FileStream ostream = new FileStream(Path.Combine(Application.dataPath, out_filepathOutTreeCoverageMap), FileMode.Create, FileAccess.Write);
                        BinaryWriter writerMapTreeCoverage = new BinaryWriter(ostream, Encoding.UTF8);
                        writerMapTreeCoverage.Write(mapTreeCoverage, 0, width * height);
                        writerMapTreeCoverage.Close();
                        ostream.Close();
                    }
                    #endif
                    //Debug.Log(string.Format("max elevation: {0}", maxElevation));
                }
                
                init = true;
            }
        }
        /// <summary>
        /// Gets map pixel data for any location in world.
        /// </summary>
        public static MapPixelData GetMapPixelData(ContentReader contentReader, int mapPixelX, int mapPixelY)
        {
            // Read general data from world maps
            int worldHeight = contentReader.WoodsFileReader.GetHeightMapValue(mapPixelX, mapPixelY);
            int worldClimate = contentReader.MapFileReader.GetClimateIndex(mapPixelX, mapPixelY);
            int worldPolitic = contentReader.MapFileReader.GetPoliticIndex(mapPixelX, mapPixelY);

            // Get location if present
            int id = -1, regionIndex = -1, mapIndex = -1;
            string locationName = string.Empty;
            ContentReader.MapSummary mapSummary = new ContentReader.MapSummary();
            bool hasLocation = contentReader.HasLocation(mapPixelX, mapPixelY, out mapSummary);
            if (hasLocation)
            {
                id = mapSummary.ID;
                regionIndex = mapSummary.RegionIndex;
                mapIndex = mapSummary.MapIndex;
                DFLocation location = contentReader.MapFileReader.GetLocation(regionIndex, mapIndex);
                locationName = location.Name;
            }

            // Create map pixel data
            MapPixelData mapPixel = new MapPixelData()
            {
                inWorld = true,
                mapPixelX = mapPixelX,
                mapPixelY = mapPixelY,
                worldHeight = worldHeight,
                worldClimate = worldClimate,
                worldPolitic = worldPolitic,
                hasLocation = hasLocation,
                mapRegionIndex = regionIndex,
                mapLocationIndex = mapIndex,
                locationID = id,
                locationName = locationName,
            };

            return mapPixel;
        }