// Based on the wikipedia koppen entry, and Table 1 from this paper: // https://opus.bibliothek.uni-augsburg.de/opus4/frontdoor/deliver/index/docId/40083/file/metz_Vol_15_No_3_p259-263_World_Map_of_the_Koppen_Geiger_climate_classification_updated_55034.pdf public static string Classify(WeatherSample sample) { if (sample.TemperatureMax < 0) { return("EF"); } if (sample.TemperatureMax >= 0 && sample.TemperatureMax < 10) { return("ET"); } var group = string.Empty; var precipitation = string.Empty; var temperature = string.Empty; // have to check before Group A because Aw is defined basically as // "Hot, but not rainy, but also not B" //Group B if (sample.TotalAnnualRainfall <= sample.DesertThreshold) { group = "B"; precipitation = (sample.TotalAnnualRainfall <= sample.DesertThreshold * .5f) ? "W" //desert : "S"; //steppe temperature = sample.TemperatureMin > 0 ? "h" : "k"; return($"{group}{precipitation}{temperature}"); } // else rainfall is above the desert threshold, this is not a Group B climate //Group A if (sample.TemperatureMin >= 18) { group = "A"; var monsoonRainfallCutoff = 25 * (100 - sample.RainfallMin); if (sample.RainfallMin >= 60) { precipitation = "f"; } else if (sample.TotalAnnualRainfall >= monsoonRainfallCutoff) { precipitation = "m"; } else if (sample.RainfallWinter < 60 && sample.RainfallWinter < sample.RainfallSummer) { precipitation = "w"; } else { precipitation = "s"; } return($"{group}{precipitation}"); } //Group C if (sample.TemperatureMin > 0) { group = "C"; if (sample.RainfallSummer < sample.RainfallWinter && sample.RainfallWinter > 3 * sample.RainfallSummer && sample.RainfallSummer < 40) { precipitation = "s"; } else if (sample.RainfallSummer > 10 * sample.RainfallWinter) { precipitation = "w"; } else { precipitation = "f"; } } //Group D if (sample.TemperatureMin <= 0) { group = "D"; if (sample.RainfallSummer < sample.RainfallWinter && sample.RainfallWinter > 3 * sample.RainfallSummer && sample.RainfallSummer < 40) { precipitation = "s"; } else if (sample.RainfallSummer > 10 * sample.RainfallWinter) { precipitation = "w"; } else { precipitation = "f"; } } if (sample.TemperatureMax >= 22) { temperature = "a"; } else if (sample.Temperatures.Count(t => t > 10) >= 2) { temperature = "b"; } else if (sample.TemperatureMin > -38) { temperature = "c"; } else { temperature = "d"; } if (group == string.Empty) { throw new Exception("Could not determine Koppen Climate Group for Sample: " + sample.ToString()); } return($"{group}{precipitation}{temperature}"); }
static void Main(string[] args) { //TODO: Make args // var summerTemperatureOffset = 0; // var winterTemperatureOffset = 0; const short hottestTemperatureOffset = 2; const short coldestTemperatureOffset = -2; // Directory.SetCurrentDirectory(args[0]); _inDir = Path.Join(Directory.GetCurrentDirectory(), "input"); _outDir = Path.Join(Directory.GetCurrentDirectory(), "output"); Console.WriteLine($"Koppen Classifier reading from: {_inDir}"); var imageInfo = new MagickImageInfo(Path.Join(_inDir, "alpha.png")); var imageWidth = imageInfo.Width; var imageHeight = imageInfo.Height; var halfHeight = imageHeight / 2; var sampleCount = imageWidth * imageHeight; Console.WriteLine($"Map Resolution: {imageWidth}x{imageHeight} ({sampleCount} Samples)"); var climateMaps = InitializeClimateMaps( imageWidth, imageHeight); Console.Write("Loading Maps... "); var landMask = ReadImage("alpha.png"); var summerRain = ReadImage("summerRain.png"); var winterRain = ReadImage("winterRain.png"); var summerTemperature = ReadImage("summerTemperature.png"); var winterTemperature = ReadImage("winterTemperature.png"); var hottestTemperature = ReadImage("hottestTemperature.png"); var coldestTemperature = ReadImage("coldestTemperature.png"); //TODO: Check that all image dimensions match Console.WriteLine("Done!"); Console.WriteLine("Classification Progress..."); void ClassifySample(int x, int y) { if (landMask[x, y][COLOR_CHANNEL] == 0) { return; } var sampleHemisphere = y <= halfHeight ? Hemisphere.North : Hemisphere.South; //TODO: Check bounds given offsets //TODO: use summer and winter offsets var sample = new WeatherSample( sampleHemisphere, ToRainfall(ReadPixel(summerRain, x, y)), ToRainfall(ReadPixel(winterRain, x, y)), ToTemperature(ReadPixel(summerTemperature, x, y)), ToTemperature(ReadPixel(winterTemperature, x, y)), (short)(ToTemperature(ReadPixel(hottestTemperature, x, y)) + hottestTemperatureOffset), (short)(ToTemperature(ReadPixel(coldestTemperature, x, y)) + coldestTemperatureOffset) ); var zone = KoppenClassifier.Classify(sample); climateMaps[zone][x, y] = true; } Console.WriteLine("0%"); var completed = 0; var cursorPos = Console.CursorTop - 1; void UpdateProgress(object _) { Console.SetCursorPosition(0, cursorPos); Console.WriteLine($"{Math.Round((completed / (float) sampleCount) * 100, 1)}% "); } var updateProgressPeriod = TimeSpan.FromSeconds(3); var watch = Stopwatch.StartNew(); using (new Timer(UpdateProgress, null, updateProgressPeriod, updateProgressPeriod)) { // for (var x = 0; x < imageWidth; x++) { // for (var y = 0; y < imageHeight; y++) { Parallel.For(0, imageWidth, x => { Parallel.For(0, imageHeight, y => { ClassifySample(x, y); Interlocked.Increment(ref completed); //threadsafe completed++ }); }); } watch.Stop(); Console.SetCursorPosition(0, cursorPos); Console.WriteLine("100.0% "); Console.WriteLine($"Classification Completed in {watch.ElapsedMilliseconds/1000f}s"); Console.WriteLine("Writing Climate Maps..."); SaveClimateZoneMaps(climateMaps); Console.WriteLine("Climate Maps Complete!"); }