diff --git a/requirements.txt b/requirements.txt index 2d6c0a54..d1f590b3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,4 +4,5 @@ numpy==1.9.2 Pillow==2.8.2 PyPlatec==1.4.0 protobuf==3.0.0a3 -six==1.10.0 +pypng==0.0.18 +six==1.10.0 \ No newline at end of file diff --git a/setup.py b/setup.py index 8ad68f8a..c4fcbfec 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ 'entry_points': { 'console_scripts': ['worldengine=worldengine.cli.main:main'], }, - 'install_requires': ['Pillow==2.8.2', 'PyPlatec==1.4.0', + 'install_requires': ['Pillow==2.8.2', 'PyPlatec==1.4.0', 'pypng>=0.0.18', 'argparse==1.2.1', 'noise==1.2.2', 'protobuf>=2.6.0', 'numpy>=1.9.2'], 'license': 'MIT License' diff --git a/tests/astar_test.py b/tests/astar_test.py index e9bde0a4..33818555 100644 --- a/tests/astar_test.py +++ b/tests/astar_test.py @@ -1,6 +1,7 @@ import unittest import numpy from worldengine import astar +from worldengine.common import _equal class TestCommon(unittest.TestCase): @@ -11,13 +12,13 @@ def test_traversal(self): line.fill(1.0) test_map[10, :] = line test_map[10, 18] = 0.0 - path_data = [[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [0, 7], [0, 8], [0, 9], - [1, 9], [2, 9], [3, 9], [4, 9], [5, 9], [6, 9], [7, 9], [8, 9], [9, 9], - [10, 9], [11, 9], [12, 9], [13, 9], [14, 9], [15, 9], [16, 9], [17, 9], - [18, 9], [18, 10], [18, 11], [18, 12], [18, 13], [18, 14], [18, 15], - [18, 16], [18, 17], [18, 18], [18, 19], [19, 19]] + path_data = numpy.array([[0, 1], [0, 2], [0, 3], [0, 4], [0, 5], [0, 6], [0, 7], [0, 8], [0, 9], + [1, 9], [2, 9], [3, 9], [4, 9], [5, 9], [6, 9], [7, 9], [8, 9], [9, 9], + [10, 9], [11, 9], [12, 9], [13, 9], [14, 9], [15, 9], [16, 9], [17, 9], + [18, 9], [18, 10], [18, 11], [18, 12], [18, 13], [18, 14], [18, 15], + [18, 16], [18, 17], [18, 18], [18, 19], [19, 19]]) shortest_path = astar.PathFinder().find(test_map, [0, 0], [19, 19]) - self.assertEqual(path_data, shortest_path) + self.assertTrue(_equal(path_data, numpy.array(shortest_path))) if __name__ == '__main__': unittest.main() diff --git a/tests/common_test.py b/tests/common_test.py index 27e0f209..fb374988 100644 --- a/tests/common_test.py +++ b/tests/common_test.py @@ -1,4 +1,5 @@ import unittest +import numpy from worldengine.common import Counter, anti_alias, array_to_matrix, get_verbose, \ matrix_min_and_max, rescale_value, set_verbose @@ -59,14 +60,14 @@ def test_rescale_value(self): self.assertAlmostEqual(10.0, rescale_value(1.0, 0.0, 1.0, -10.0, 10.0)) def test_antialias(self): - original = [[0.5, 0.12, 0.7, 0.15, 0.0], - [0.0, 0.12, 0.7, 0.7, 8.0], - [0.2, 0.12, 0.7, 0.7, 4.0]] + original = numpy.array([[0.5, 0.12, 0.7, 0.15, 0.0], + [0.0, 0.12, 0.7, 0.7, 8.0], + [0.2, 0.12, 0.7, 0.7, 4.0]]) antialiased = anti_alias(original, 1) self.assertAlmostEquals(1.2781818181818183, antialiased[0][0]) self.assertAlmostEquals(0.4918181818181818, antialiased[1][2]) - original = [[0.8]] + original = numpy.array([[0.8]]) antialiased = anti_alias(original, 10) self.assertAlmostEquals(0.8, antialiased[0][0]) diff --git a/tests/serialization_test.py b/tests/serialization_test.py index 264fbb24..38839e47 100644 --- a/tests/serialization_test.py +++ b/tests/serialization_test.py @@ -1,16 +1,9 @@ import unittest from worldengine.plates import Step, world_gen from worldengine.world import World +from worldengine.common import _equal import tempfile import os -import numpy - - -def _sort(l): - l2 = l - l2.sort() - return l2 - class TestSerialization(unittest.TestCase): @@ -23,48 +16,48 @@ def test_pickle_serialize_unserialize(self): w.to_pickle_file(f) unserialized = World.from_pickle_file(f) os.remove(f) - self.assertEqual(w.elevation['data'], unserialized.elevation['data']) + self.assertTrue(_equal(w.elevation['data'], unserialized.elevation['data'])) self.assertEqual(w.elevation['thresholds'], unserialized.elevation['thresholds']) - self.assertEqual(w.ocean, unserialized.ocean) - self.assertEqual(w.biome, unserialized.biome) - self.assertEqual(w.humidity, unserialized.humidity) - self.assertTrue(numpy.array_equiv(w.irrigation, unserialized.irrigation)) - self.assertEqual(w.permeability, unserialized.permeability) - self.assertEqual(w.watermap, unserialized.watermap) - self.assertEqual(w.precipitation, unserialized.precipitation) - self.assertEqual(w.temperature, unserialized.temperature) - self.assertEqual(w.sea_depth, unserialized.sea_depth) + self.assertTrue(_equal(w.ocean, unserialized.ocean)) + self.assertTrue(_equal(w.biome, unserialized.biome)) + self.assertTrue(_equal(w.humidity, unserialized.humidity)) + self.assertTrue(_equal(w.irrigation, unserialized.irrigation)) + self.assertTrue(_equal(w.permeability, unserialized.permeability)) + self.assertTrue(_equal(w.watermap, unserialized.watermap)) + self.assertTrue(_equal(w.precipitation, unserialized.precipitation)) + self.assertTrue(_equal(w.temperature, unserialized.temperature)) + self.assertTrue(_equal(w.sea_depth, unserialized.sea_depth)) self.assertEquals(w.seed, unserialized.seed) self.assertEquals(w.n_plates, unserialized.n_plates) - self.assertEquals(w.ocean_level, unserialized.ocean_level) - self.assertEquals(w.lake_map, unserialized.lake_map) - self.assertEquals(w.river_map, unserialized.river_map) + self.assertTrue(_equal(w.ocean_level, unserialized.ocean_level)) + self.assertTrue(_equal(w.lake_map, unserialized.lake_map)) + self.assertTrue(_equal(w.river_map, unserialized.river_map)) self.assertEquals(w.step, unserialized.step) - self.assertEqual(_sort(dir(w)), _sort(dir(unserialized))) + self.assertEqual(sorted(dir(w)), sorted(dir(unserialized))) self.assertEqual(w, unserialized) def test_protobuf_serialize_unserialize(self): w = world_gen("Dummy", 32, 16, 1, step=Step.get_by_name("full")) serialized = w.protobuf_serialize() unserialized = World.protobuf_unserialize(serialized) - self.assertEqual(w.elevation['data'], unserialized.elevation['data']) + self.assertTrue(_equal(w.elevation['data'], unserialized.elevation['data'])) self.assertEqual(w.elevation['thresholds'], unserialized.elevation['thresholds']) - self.assertEqual(w.ocean, unserialized.ocean) - self.assertEqual(w.biome, unserialized.biome) - self.assertEqual(w.humidity, unserialized.humidity) - self.assertTrue(numpy.array_equiv(w.irrigation, unserialized.irrigation)) - self.assertEqual(w.permeability, unserialized.permeability) - self.assertEqual(w.watermap, unserialized.watermap) - self.assertEqual(w.precipitation, unserialized.precipitation) - self.assertEqual(w.temperature, unserialized.temperature) - self.assertEqual(w.sea_depth, unserialized.sea_depth) + self.assertTrue(_equal(w.ocean, unserialized.ocean)) + self.assertTrue(_equal(w.biome, unserialized.biome)) + self.assertTrue(_equal(w.humidity, unserialized.humidity)) + self.assertTrue(_equal(w.irrigation, unserialized.irrigation)) + self.assertTrue(_equal(w.permeability, unserialized.permeability)) + self.assertTrue(_equal(w.watermap, unserialized.watermap)) + self.assertTrue(_equal(w.precipitation, unserialized.precipitation)) + self.assertTrue(_equal(w.temperature, unserialized.temperature)) + self.assertTrue(_equal(w.sea_depth, unserialized.sea_depth)) self.assertEquals(w.seed, unserialized.seed) self.assertEquals(w.n_plates, unserialized.n_plates) - self.assertEquals(w.ocean_level, unserialized.ocean_level) - self.assertEquals(w.lake_map, unserialized.lake_map) - self.assertEquals(w.river_map, unserialized.river_map) + self.assertTrue(_equal(w.ocean_level, unserialized.ocean_level)) + self.assertTrue(_equal(w.lake_map, unserialized.lake_map)) + self.assertTrue(_equal(w.river_map, unserialized.river_map)) self.assertEquals(w.step, unserialized.step) - self.assertEqual(_sort(dir(w)), _sort(dir(unserialized))) + self.assertEqual(sorted(dir(w)), sorted(dir(unserialized))) self.assertEqual(w, unserialized) diff --git a/tox.ini b/tox.ini index 5b7deae3..dc751222 100644 --- a/tox.ini +++ b/tox.ini @@ -11,6 +11,7 @@ deps = nose protobuf six + pypng [testenv] deps = diff --git a/worldengine/astar.py b/worldengine/astar.py index 5fee0506..9e1ab2ff 100644 --- a/worldengine/astar.py +++ b/worldengine/astar.py @@ -161,7 +161,6 @@ def get_node(self, location): if x < 0 or x >= self.w or y < 0 or y >= self.h: return None d = self.m[(y * self.w) + x] - return Node(location, d, ((y * self.w) + x)) def get_adjacent_nodes(self, cur_node, destination): @@ -198,14 +197,6 @@ def _handle_node(self, x, y, from_node, destination_x, destination_y): return None -def _matrix_to_array(matrix): - array = [] - for row in matrix: - for cell in row: - array.append(cell) - return array - - class PathFinder: """Using the a* algorithm we will try to find the best path between two points. @@ -219,10 +210,9 @@ def find(height_map, source, destination): sx, sy = source dx, dy = destination path = [] - width = len(height_map[0]) - height = len(height_map) + height, width = height_map.shape - graph = _matrix_to_array(height_map) # flatten array + graph = height_map.flatten('C') #flatten array (row-major) pathfinder = AStar(SQMapHandler(graph, width, height)) start = SQLocation(sx, sy) diff --git a/worldengine/biome.py b/worldengine/biome.py index 2a37f2cd..24e971c5 100644 --- a/worldengine/biome.py +++ b/worldengine/biome.py @@ -219,6 +219,6 @@ def biome_name_to_index(biome_name): def biome_index_to_name(biome_index): names = sorted(_BiomeMetaclass.biomes.keys()) - if biome_index < 0 or biome_index >= len(names): + if not 0 <= biome_index < len(names): raise Exception("Not found") return names[biome_index] diff --git a/worldengine/cli/main.py b/worldengine/cli/main.py index cc5ca9b9..38044813 100644 --- a/worldengine/cli/main.py +++ b/worldengine/cli/main.py @@ -1,6 +1,7 @@ import sys from argparse import ArgumentParser import os +import numpy import pickle import random import worldengine.generation as geo @@ -95,7 +96,7 @@ def generate_plates(seed, world_name, output_dir, width, height, num_plates=num_plates) world = World(world_name, width, height, seed, num_plates, -1.0, "plates") - world.set_elevation(array_to_matrix(elevation, width, height), None) + world.set_elevation(numpy.array(elevation).reshape(height, width), None) world.set_plates(array_to_matrix(plates, width, height)) # Generate images @@ -341,7 +342,8 @@ def main(): export_options = parser.add_argument_group( "Export Options", "You can specify the formats you wish the generated output to be in. ") export_options.add_argument("--export-type", dest="export_type", - help="Export to a specific format such as: BMP or PNG", + help="Export to a specific format such as: BMP or PNG" + + "See http://www.gdal.org/formats_list.html for possible formats.", default="bmp") export_options.add_argument("--export-bpp", dest="export_bpp", type=int, help="Bits per pixel: 8, 16 and 32", @@ -448,6 +450,7 @@ def main(): seed, args.number_of_plates, args.output_dir, step, args.ocean_level, world_format, args.verbose, black_and_white=args.black_and_white) + if args.grayscale_heightmap: generate_grayscale_heightmap(world, '%s/%s_grayscale.png' % (args.output_dir, world_name)) @@ -493,7 +496,8 @@ def main(): elif operation == 'export': world = load_world(args.FILE) print_world_info(world) - export(world, args.export_type, args.export_bpp, args.export_signed, args.export_normalize) + export(world, args.export_type, args.export_bpp, args.export_signed, + path = '%s/%s_elevation' % (args.output_dir, world_name)) else: raise Exception( 'Unknown operation: valid operations are %s' % OPERATIONS) diff --git a/worldengine/common.py b/worldengine/common.py index 0decf046..17725090 100644 --- a/worldengine/common.py +++ b/worldengine/common.py @@ -1,5 +1,6 @@ import sys import copy +import numpy #for the _equal method only # ---------------- # Global variables @@ -80,32 +81,31 @@ def rescale_value(original, prev_min, prev_max, min, max): return min + ((max - min) * f) -def anti_alias(elevation, steps): +def anti_alias(map, steps):#TODO: There is probably a bit of numpy-optimization that can be done here. """ - Execute the anti_alias operation steps times on the given elevation map + Execute the anti_alias operation steps times on the given map """ - width = len(elevation[0]) - height = len(elevation) + height, width = map.shape def _anti_alias_step(original): anti_aliased = copy.deepcopy(original) for y in range(height): for x in range(width): - anti_aliased[y][x] = anti_alias_point(original, x, y) + anti_aliased[y, x] = anti_alias_point(original, x, y) return anti_aliased def anti_alias_point(original, x, y): n = 2 - tot = elevation[y][x] * 2 + tot = map[y, x] * 2 for dy in range(-1, +2): py = (y + dy) % height for dx in range(-1, +2): px = (x + dx) % width n += 1 - tot += original[py][px] + tot += original[py, px] return tot / n - current = elevation + current = map for i in range(steps): current = _anti_alias_step(current) return current @@ -120,3 +120,33 @@ def array_to_matrix(array, width, height): for x in range(width): matrix[y].append(array[y * width + x]) return matrix + +def _equal(a, b): + #recursion on subclasses of types: tuple, list, dict + #specifically checks : float, ndarray + if type(a) is float and type(b) is float:#float + return(numpy.allclose(a, b)) + elif type(a) is numpy.ndarray and type(b) is numpy.ndarray:#ndarray + return(numpy.array_equiv(a, b))#alternative for float-arrays: numpy.allclose(a, b[, rtol, atol]) + elif isinstance(a, dict) and isinstance(b, dict):#dict + if len(a) != len(b): + return(False) + t = True + for key, val in a.items(): + if key not in b: + return(False) + t = _equal(val, b[key]) + if not t: + return(False) + return(t) + elif (isinstance(a, list) and isinstance(b, list)) or (isinstance(a, tuple) and isinstance(b, tuple)):#list, tuples + if len(a) != len(b): + return(False) + t = True + for vala, valb in zip(a, b): + t = _equal(vala, valb) + if not t: + return(False) + return(t) + else:#fallback + return(a == b) diff --git a/worldengine/draw.py b/worldengine/draw.py index 06d16c66..cc15f312 100644 --- a/worldengine/draw.py +++ b/worldengine/draw.py @@ -1,7 +1,12 @@ from PIL import Image +import numpy +import png +#Documentation PyPNG: https://pythonhosted.org/pypng/png.html +#Documentation PurePNG: http://purepng.readthedocs.org/en/latest/ +#The latter one is a fork of the former one. Don't know which is better, but one will hopefully survive for a long time. :P from worldengine.drawing_functions import draw_ancientmap, \ - draw_rivers_on_image, gradient + draw_rivers_on_image # ------------- # Helper values @@ -129,8 +134,43 @@ def elevation_color(elevation, sea_level=1.0): # Draw on generic target # ---------------------- +class MapToPNGPrinter(object): + def __init__(self, map, filename, grayscale = True):#TODO: currently only works for 16 bit grayscale, might want to adapt it to other specifications; PyPNG supposedly can handle them all + self.img = png.Writer(width = map.shape[1], height = map.shape[0], greyscale = grayscale, bitdepth = 16)#British spelling + self.map = map + self.filename = filename + self.grayscale = grayscale + + @classmethod + def from_dimensions(cls, width, height, filename, grayscale = True): + _map = numpy.zeros((height, width), dtype = numpy.uint16) + return cls(_map, filename, grayscale) + + @classmethod + def from_map(cls, map, filename, grayscale = True, scale_to_range = True): + if scale_to_range: + max = map.max() + min = map.min() + _map = (2**16 - 1) * (map - min) / (max - min) + _map = _map.astype(dtype = numpy.uint16) + return cls(_map, filename, grayscale) -class ImagePixelSetter(object): + def set_pixel(self, x, y, color):#TODO: currently only works for 16 bit grayscale + self.map[y, x] = color + + def complete(self): + with open(self.filename, 'wb') as f: + out = self.map.tolist()#convert to Python list of lists as expected by the png-writer + self.img.write(f, out) + + def __getitem__(self, item): + return self.map[item] + + def __setitem__(self, item, value): + self.map[item] = value + + +class ImagePixelSetter(object):#TODO: right now only works for colored images, can probably be combined with MapToPNGPrinter-class def __init__(self, width, height, filename): self.img = Image.new('RGBA', (width, height)) @@ -166,7 +206,7 @@ def draw_simple_elevation(data, width, height, sea_level, target): """ for y in range(height): for x in range(width): - e = data[y][x] + e = data[y, x] r, g, b = elevation_color(e, sea_level) target.set_pixel(x, y, (int(r * 255), int(g * 255), int(b * 255), 255)) @@ -178,7 +218,7 @@ def draw_riversmap(world, target): for y in range(world.height): for x in range(world.width): - if world.ocean[y][x]: + if world.ocean[y, x]: target.set_pixel(x, y, sea_color) else: target.set_pixel(x, y, land_color) @@ -187,35 +227,44 @@ def draw_riversmap(world, target): def draw_grayscale_heightmap(world, target): - min_elev_sea = None - max_elev_sea = None - min_elev_land = None - max_elev_land = None - for y in range(world.height): - for x in range(world.width): - e = world.elevation['data'][y][x] - if world.is_land((x, y)): - if min_elev_land is None or e < min_elev_land: - min_elev_land = e - if max_elev_land is None or e > max_elev_land: - max_elev_land = e - else: - if min_elev_sea is None or e < min_elev_sea: - min_elev_sea = e - if max_elev_sea is None or e > max_elev_sea: - max_elev_sea = e - - elev_delta_land = max_elev_land - min_elev_land - elev_delta_sea = max_elev_sea - min_elev_sea + #target: ImagePixelSetter object (writes 8 Bit grayscale) + #mask = numpy.ma.array(world.elevation['data'], mask = world.ocean) + #min_elev_land = mask.min() + #max_elev_land = mask.max() + + #mask.mask = numpy.logical_not(mask.mask) + #min_elev_sea = mask.min() + #max_elev_sea = mask.max() + + #elev_delta_land = max_elev_land - min_elev_land + #elev_delta_sea = max_elev_sea - min_elev_sea + + total_min = world.elevation['data'].min() + total_max = world.elevation['data'].max() + elev_delta = total_max - total_min + + bpp = 8 + printer = False + if type(target) is MapToPNGPrinter: + bpp = 16 + printer = True + halfcol = 2**(bpp - 1) - 1 for y in range(world.height): for x in range(world.width): - e = world.elevation['data'][y][x] + e = world.elevation['data'][y, x] + '''This comment is just for clarification and should be removed soon. + Note: The previous method (in comments) does not allow for true + depressions in the output *and* if depressions existed in the input, + the landmass will be lifted up, creating very unrealistic coasts. + ''' if world.is_land((x, y)): - c = int(((e - min_elev_land) * 127) / elev_delta_land)+128 + #c = int(halfcol * (e - min_elev_land) / elev_delta_land) + halfcol + 1 + c = int((2 * halfcol + 1) * (e - total_min) / elev_delta) else: - c = int(((e - min_elev_sea) * 127) / elev_delta_sea) - target.set_pixel(x, y, (c, c, c, 255)) + #c = int(halfcol * (e - min_elev_sea) / elev_delta_sea) + c = int((2 * halfcol + 1) * (e - total_min) / elev_delta) + target.set_pixel(x, y, c if printer else (c, c, c)) def draw_elevation(world, shadow, target): @@ -225,34 +274,28 @@ def draw_elevation(world, shadow, target): data = world.elevation['data'] ocean = world.ocean - min_elev = None - max_elev = None - for y in range(height): - for x in range(width): - if not ocean[y][x]: - e = data[y][x] - if min_elev is None or e < min_elev: - min_elev = e - if max_elev is None or e > max_elev: - max_elev = e + mask = numpy.ma.array(data, mask = ocean) + + min_elev = mask.min() + max_elev = mask.max() elev_delta = max_elev - min_elev for y in range(height): for x in range(width): - if ocean[y][x]: + if ocean[y, x]: target.set_pixel(x, y, (0, 0, 255, 255)) else: - e = data[y][x] + e = data[y, x] c = 255 - int(((e - min_elev) * 255) / elev_delta) if shadow and y > 2 and x > 2: - if data[y - 1][x - 1] > e: + if data[y - 1, x - 1] > e: c -= 15 - if data[y - 2][x - 2] > e \ - and data[y - 2][x - 2] > data[y - 1][x - 1]: + if data[y - 2, x - 2] > e \ + and data[y - 2, x - 2] > data[y - 1, x - 1]: c -= 10 - if data[y - 3][x - 3] > e \ - and data[y - 3][x - 3] > data[y - 1][x - 1] \ - and data[y - 3][x - 3] > data[y - 2][x - 2]: + if data[y - 3, x - 3] > e \ + and data[y - 3, x - 3] > data[y - 1, x - 1] \ + and data[y - 3, x - 3] > data[y - 2, x - 2]: c -= 5 if c < 0: c = 0 @@ -260,41 +303,35 @@ def draw_elevation(world, shadow, target): def draw_ocean(ocean, target): - width = len(ocean[0]) - height = len(ocean) + height, width = ocean.shape for y in range(height): for x in range(width): - if ocean[y][x]: + if ocean[y, x]: target.set_pixel(x, y, (0, 0, 255, 255)) else: target.set_pixel(x, y, (0, 255, 255, 255)) def draw_precipitation(world, target, black_and_white=False): - # FIXME we are drawing humidity, not precipitations + #TODO: FIXME we are drawing humidity, not precipitations width = world.width height = world.height if black_and_white: - low = None - high = None - for y in range(height): - for x in range(width): - p = world.precipitations_at((x, y)) - if low is None or p < low: - low = p - if high is None or p > high: - high = p - for y in range(height): + floor = 0 + ceiling = 255 + if type(target) is MapToPNGPrinter: + ceiling = 65535 + + low = world.precipitation['data'].min() + high = world.precipitation['data'].max() + for y in range(height):#TODO: the following could easily be done on the whole array at once, but target might be of a type that cannot write a complete array for x in range(width): p = world.precipitations_at((x, y)) - if p <= low: - target.set_pixel(x, y, (0, 0, 0, 255)) - elif p >= high: - target.set_pixel(x, y, (255, 255, 255, 255)) - else: - target.set_pixel(x, y, gradient(p, low, high, (0, 0, 0), (255, 255, 255))) + c = numpy.interp(p, [low, high], [floor, ceiling]) + c = int(numpy.rint(c))#proper rounding + target.set_pixel(x, y, c if ceiling == 65535 else (c, c, c)) else: for y in range(height): for x in range(width): @@ -326,7 +363,7 @@ def draw_world(world, target): biome = world.biome_at((x, y)) target.set_pixel(x, y, _biome_colors[biome.name()]) else: - c = int(world.sea_depth[y][x] * 200 + 50) + c = int(world.sea_depth[y, x] * 200 + 50) target.set_pixel(x, y, (0, 0, 255 - c, 255)) @@ -335,17 +372,19 @@ def draw_temperature_levels(world, target, black_and_white=False): height = world.height if black_and_white: + floor = 0 + ceiling = 255 + if type(target) is MapToPNGPrinter: + ceiling = 65535 + low = world.temperature_thresholds()[0][1] high = world.temperature_thresholds()[5][1] - for y in range(height): + for y in range(height):#TODO: the following could easily be done on the whole array at once, but target might be of a type that cannot write a complete array for x in range(width): t = world.temperature_at((x, y)) - if t <= low: - target.set_pixel(x, y, (0, 0, 0, 255)) - elif t >= high: - target.set_pixel(x, y, (255, 255, 255, 255)) - else: - target.set_pixel(x, y, gradient(t, low, high, (0, 0, 0), (255, 255, 255))) + c = numpy.interp(t, [low, high], [floor, ceiling]) + c = int(numpy.rint(c)) + target.set_pixel(x, y, c if ceiling == 65535 else (c, c, c)) else: for y in range(height): @@ -374,7 +413,7 @@ def draw_biome(world, target): for y in range(height): for x in range(width): - v = biome[y][x] + v = biome[y, x] target.set_pixel(x, y, _biome_colors[v]) @@ -396,8 +435,9 @@ def draw_riversmap_on_file(world, filename): def draw_grayscale_heightmap_on_file(world, filename): - img = ImagePixelSetter(world.width, world.height, filename) - draw_grayscale_heightmap(world, img) + img = MapToPNGPrinter.from_map(world.elevation['data'], filename, grayscale = True, scale_to_range = True) + #img = ImagePixelSetter(world.width, world.height, filename) + #draw_grayscale_heightmap(world, img) img.complete() @@ -408,15 +448,16 @@ def draw_elevation_on_file(world, filename, shadow=True): def draw_ocean_on_file(ocean, filename): - width = len(ocean[0]) - height = len(ocean) - img = ImagePixelSetter(width, height, filename) + img = ImagePixelSetter(ocean.shape[1], ocean.shape[0], filename) draw_ocean(ocean, img) img.complete() -def draw_precipitation_on_file(world, filename, black_and_white=False): - img = ImagePixelSetter(world.width, world.height, filename) +def draw_precipitation_on_file(world, filename, black_and_white=False):#TODO: find out if 16 bit is appropriate for black_and_white output + if black_and_white: + img = MapToPNGPrinter.from_map(world.precipitation['data'], filename, grayscale = black_and_white, scale_to_range = True) + else: + img = ImagePixelSetter(world.width, world.height, filename) draw_precipitation(world, img, black_and_white) img.complete() @@ -427,8 +468,11 @@ def draw_world_on_file(world, filename): img.complete() -def draw_temperature_levels_on_file(world, filename, black_and_white=False): - img = ImagePixelSetter(world.width, world.height, filename) +def draw_temperature_levels_on_file(world, filename, black_and_white=False):#TODO: find out if 16 bit is appropriate for black_and_white output + if black_and_white: + img = MapToPNGPrinter.from_map(world.temperature['data'], filename, grayscale = black_and_white, scale_to_range = True) + else: + img = ImagePixelSetter(world.width, world.height, filename) draw_temperature_levels(world, img, black_and_white) img.complete() diff --git a/worldengine/drawing_functions.py b/worldengine/drawing_functions.py index dc72a5cd..8f7aa25a 100644 --- a/worldengine/drawing_functions.py +++ b/worldengine/drawing_functions.py @@ -5,6 +5,7 @@ """ import math +import numpy import random import sys import time @@ -46,8 +47,8 @@ def _lowest_neighbour(world, pos): for dx in range(-1, 1): for dy in range(-1, 1): if dx != 0 or dy != 0: - e = world.elevation['data'][y + dy][ - x + dx] # +world.humidity['data'][y+dy][x+dx]/3.0 + e = world.elevation['data'][y + dy, + x + dx] # +world.humidity['data'][y+dy, x+dx]/3.0 if (not lowest_lvl) or (e < lowest_lvl): lowest_lvl = e lowest = (x + dx, y + dy) @@ -58,7 +59,7 @@ def _lowest_neighbour(world, pos): return lowest def _draw_river(world, target, pos, factor): - if world.is_ocean(pos): + if world.ocean[pos[1], pos[0]]: return x, y = pos for dx in range(factor): @@ -76,9 +77,9 @@ def _draw_river(world, target, pos, factor): cc = None for c in candidates: cx, cy = c - wl = world.humidity['data'][cy][cx] * \ - world.precipitation['data'][cy][cx] * \ - world.elevation['data'][cy][cx] + wl = world.humidity['data'][cy, cx] * \ + world.precipitation['data'][cy, cx] * \ + world.elevation['data'][cy, cx] if max is None or wl > max: max = wl cc = c @@ -90,50 +91,48 @@ def _draw_river(world, target, pos, factor): # ------------------- def _find_land_borders(world, factor): - _ocean = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] - _borders = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] - for y in range(world.height * factor): + _ocean = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) + _borders = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) + + #scale ocean + for y in range(world.height * factor):#TODO: numpy for x in range(world.width * factor): - if world.ocean[int(y / factor)][int(x / factor)]: - _ocean[y][x] = True + if world.ocean[int(y / factor), int(x / factor)]: + _ocean[y, x] = True def my_is_ocean(pos): - x, y = pos - return _ocean[y][x] + return _ocean[pos[1], pos[0]] for y in range(world.height * factor): for x in range(world.width * factor): - if not _ocean[y][x] and world.tiles_around_factor(factor, (x, y), radius=1, predicate=my_is_ocean): - _borders[y][x] = True + if not _ocean[y, x] and world.tiles_around_factor(factor, (x, y), radius=1, predicate=my_is_ocean): + _borders[y, x] = True return _borders def _find_outer_borders(world, factor, inner_borders): - _ocean = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] - _borders = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] - for y in range(world.height * factor): + _ocean = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) + _borders = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) + + #scale ocean + for y in range(world.height * factor):#TODO: numpy for x in range(world.width * factor): if world.ocean[int(y / factor)][int(x / factor)]: - _ocean[y][x] = True + _ocean[y, x] = True def is_inner_border(pos): x, y = pos - return inner_borders[y][x] + return inner_borders[y, x] for y in range(world.height * factor): for x in range(world.width * factor): - if _ocean[y][x] and not inner_borders[y][x] and world.tiles_around_factor(factor, (x, y), radius=1, predicate=is_inner_border): - _borders[y][x] = True + if _ocean[y, x] and not inner_borders[y, x] and world.tiles_around_factor(factor, (x, y), radius=1, predicate=is_inner_border): + _borders[y, ] = True return _borders def _find_mountains_mask(world, factor): - _mask = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] + _mask = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) for y in range(factor * world.height): for x in range(factor * world.width): if world.is_mountain((int(x / factor), int(y / factor))): @@ -141,13 +140,12 @@ def _find_mountains_mask(world, factor): radius=3, predicate=world.is_mountain)) if v > 32: - _mask[y][x] = v / 4 + _mask[y, x] = v / 4 return _mask def _mask(world, predicate, factor): - _mask = [[False for x in range(factor * world.width)] for y in - range(factor * world.height)] + _mask = numpy.zeros((factor * world.height, factor * world.width), dtype=bool) for y in range(factor * world.height): for x in range(factor * world.width): xf = int(x / factor) @@ -157,7 +155,7 @@ def _mask(world, predicate, factor): world.tiles_around((xf, yf), radius=1, predicate=predicate)) if v > 5: - _mask[y][x] = v + _mask[y, x] = v return _mask @@ -592,7 +590,7 @@ def _draw_a_mountain(pixels, x, y, w=3, h=3): def _pseudo_random_land_pos(world, i): y = (i ** 7 + i * 23) % world.height x = (i ** 13 + i * 37) % world.width - if world.is_land((x, y)): + if not world.ocean[y, x]: return int(x), int(y) else: return _pseudo_random_land_pos(world, (i % 123456789) * 17 + 11) @@ -638,64 +636,49 @@ def draw_ancientmap(world, target, resize_factor=1, rock_desert_mask = _mask(world, world.is_hot_desert, resize_factor) # TODO: add is_desert_mask def unset_mask(pos): - x, y = pos - mountains_mask[y][x] = False + mountains_mask[pos[1], pos[0]] = False def unset_boreal_forest_mask(pos): - x, y = pos - boreal_forest_mask[y][x] = False + boreal_forest_mask[pos[1], pos[0]] = False def unset_temperate_forest_mask(pos): - x, y = pos - temperate_forest_mask[y][x] = False + temperate_forest_mask[pos[1], pos[0]] = False def unset_warm_temperate_forest_mask(pos): - x, y = pos - warm_temperate_forest_mask[y][x] = False + warm_temperate_forest_mask[pos[1], pos[0]] = False def unset_tropical_dry_forest_mask(pos): - x, y = pos - tropical_dry_forest_mask[y][x] = False + tropical_dry_forest_mask[pos[1], pos[0]] = False def unset_jungle_mask(pos): - x, y = pos - jungle_mask[y][x] = False + jungle_mask[pos[1], pos[0]] = False def unset_tundra_mask(pos): - x, y = pos - tundra_mask[y][x] = False + tundra_mask[pos[1], pos[0]] = False def unset_savanna_mask(pos): - x, y = pos - savanna_mask[y][x] = False + savanna_mask[pos[1], pos[0]] = False def unset_hot_desert_mask(pos): - x, y = pos - hot_desert_mask[y][x] = False + hot_desert_mask[pos[1], pos[0]] = False def unset_rock_desert_mask(pos): - x, y = pos - rock_desert_mask[y][x] = False + rock_desert_mask[pos[1], pos[0]] = False def unset_cold_parklands_mask(pos): - x, y = pos - cold_parklands_mask[y][x] = False + cold_parklands_mask[pos[1], pos[0]] = False def unset_steppe_mask(pos): - x, y = pos - steppe_mask[y][x] = False + steppe_mask[pos[1], pos[0]] = False def unset_cool_desert_mask(pos): - x, y = pos - cool_desert_mask[y][x] = False + cool_desert_mask[pos[1], pos[0]] = False def unset_chaparral_mask(pos): - x, y = pos - chaparral_mask[y][x] = False + chaparral_mask[pos[1], pos[0]] = False def on_border(pos): - x, y = pos - return borders[y][x] + return borders[pos[1], pos[0]] if verbose: elapsed_time = time.time() - start_time @@ -706,15 +689,9 @@ def on_border(pos): if verbose: start_time = time.time() - min_elev = None - max_elev = None - for y in range(world.height): - for x in range(world.width): - e = world.elevation['data'][y][x] - if min_elev is None or e < min_elev: - min_elev = e - if max_elev is None or e > max_elev: - max_elev = e + #min_elev = world.elevation['data'].min() + #max_elev = world.elevation['data'].max() + # elev_delta = max_elev - min_elev # TODO: no longer used? if verbose: elapsed_time = time.time() - start_time @@ -730,11 +707,11 @@ def on_border(pos): for x in range(resize_factor * world.width): xf = int(x / resize_factor) yf = int(y / resize_factor) - if borders[y][x]: + if borders[y, x]: target.set_pixel(x, y, border_color) - elif draw_outer_land_border and outer_borders[y][x]: + elif draw_outer_land_border and outer_borders[y, x]: target.set_pixel(x, y, outer_border_color) - elif world.ocean[yf][xf]: + elif world.ocean[yf, xf]: target.set_pixel(x, y, sea_color) else: target.set_pixel(x, y, land_color) diff --git a/worldengine/generation.py b/worldengine/generation.py index 89619b57..8903516f 100644 --- a/worldengine/generation.py +++ b/worldengine/generation.py @@ -9,7 +9,8 @@ from worldengine.simulations.erosion import ErosionSimulation from worldengine.simulations.precipitation import PrecipitationSimulation from worldengine.simulations.biome import BiomeSimulation -from worldengine.common import anti_alias, get_verbose, matrix_min_and_max, rescale_value +from worldengine.common import anti_alias, get_verbose +import numpy # ------------------ @@ -24,9 +25,7 @@ def center_land(world): y_with_min_sum = None latshift = 0 for y in range(world.height): - sum_on_y = 0 - for x in range(world.width): - sum_on_y += world.elevation['data'][y][x] + sum_on_y = world.elevation['data'][y].sum() if min_sum_on_y is None or sum_on_y < min_sum_on_y: min_sum_on_y = sum_on_y y_with_min_sum = y @@ -36,16 +35,14 @@ def center_land(world): min_sum_on_x = None x_with_min_sum = None for x in range(world.width): - sum_on_x = 0 - for y in range(world.height): - sum_on_x += world.elevation['data'][y][x] + sum_on_x = world.elevation['data'].T[x].sum() if min_sum_on_x is None or sum_on_x < min_sum_on_x: min_sum_on_x = sum_on_x x_with_min_sum = x if get_verbose(): print("geo.center_land: width complete") - new_elevation_data = [] + new_elevation_data = [] #TODO: this should fully use numpy new_plates = [] for y in range(world.height): new_elevation_data.append([]) @@ -53,9 +50,9 @@ def center_land(world): src_y = (y_with_min_sum + y - latshift) % world.height for x in range(world.width): src_x = (x_with_min_sum + x) % world.width - new_elevation_data[y].append(world.elevation['data'][src_y][src_x]) + new_elevation_data[y].append(world.elevation['data'][src_y, src_x]) new_plates[y].append(world.plates[src_y][src_x]) - world.elevation['data'] = new_elevation_data + world.elevation['data'] = numpy.array(new_elevation_data) world.plates = new_plates if get_verbose(): print("geo.center_land: width complete") @@ -69,8 +66,8 @@ def place_oceans_at_map_borders(world): ocean_border = int(min(30, max(world.width / 5, world.height / 5))) def place_ocean(x, y, i): - world.elevation['data'][y][x] = \ - (world.elevation['data'][y][x] * i) / ocean_border + world.elevation['data'][y, x] = \ + (world.elevation['data'][y, x] * i) / ocean_border for x in range(world.width): for i in range(ocean_border): @@ -89,31 +86,30 @@ def add_noise_to_elevation(world, seed): for y in range(world.height): for x in range(world.width): n = snoise2(x / freq * 2, y / freq * 2, octaves, base=seed) - world.elevation['data'][y][x] += n + world.elevation['data'][y, x] += n -def fill_ocean(elevation, sea_level): - width = len(elevation[0]) - height = len(elevation) +def fill_ocean(elevation, sea_level):#TODO: Make more use of numpy? + height, width = elevation.shape - ocean = [[False for x in range(width)] for y in range(height)] # TODO: use numpy + ocean = numpy.zeros(elevation.shape, dtype=bool) to_expand = [] - for x in range(width): - if elevation[0][x] <= sea_level: + for x in range(width):#handle top and bottom border of the map + if elevation[0, x] <= sea_level: to_expand.append((x, 0)) - if elevation[height - 1][x] <= sea_level: + if elevation[height - 1, x] <= sea_level: to_expand.append((x, height - 1)) - for y in range(height): - if elevation[y][0] <= sea_level: + for y in range(height):#handle left- and rightmost border of the map + if elevation[y, 0] <= sea_level: to_expand.append((0, y)) - if elevation[y][width - 1] <= sea_level: + if elevation[y, width - 1] <= sea_level: to_expand.append((width - 1, y)) for t in to_expand: tx, ty = t - if not ocean[ty][tx]: - ocean[ty][tx] = True + if not ocean[ty, tx]: + ocean[ty, tx] = True for px, py in _around(tx, ty, width, height): - if not ocean[py][px] and elevation[py][px] <= sea_level: + if not ocean[py, px] and elevation[py, px] <= sea_level: to_expand.append((px, py)) return ocean @@ -128,8 +124,18 @@ def initialize_ocean_and_thresholds(world, ocean_level=1.0): """ e = world.elevation['data'] ocean = fill_ocean(e, ocean_level) - hl = find_threshold_f(e, 0.10) - ml = find_threshold_f(e, 0.03) + + #TODO: The thresholds (and amounts of e.g. mountains) could change for every world, depending on the height above the ocean. + #Some things that could be useful but haven't made it into a fleshed-out idea yet: + #shifted_elevation = e - ocean_level#shift the elevation so that ocean_level is at zero + #shifted_elevation = numpy.ma.array(shifted_elevation, mask = shifted_elevation < 0.0)#mask out areas below the oceans surface + #max = shifted_elevation.max()#height of the highest point on land relative to the oceans surface + #shifted_elevation = numpy.ma.array(e, mask = shifted_elevation.mask)#shift back but keep the mask + #hl = 0.3 + ocean_level#example for a scaled world: ocean_level + 600.0m + #ml = 1.7 + ocean_level#example for a scaled world: ocean_level + 900.0m + + hl = find_threshold_f(e, 0.10)#the highest 10% of *all* land are declared hills + ml = find_threshold_f(e, 0.03)#the highest 3% are declared mountains e_th = [('sea', ocean_level), ('plain', hl), ('hill', ml), @@ -144,26 +150,24 @@ def initialize_ocean_and_thresholds(world, ocean_level=1.0): # ---- def sea_depth(world, sea_level): - sea_depth = [[sea_level - world.elevation['data'][y][x] - for x in range(world.width)] for y in range(world.height)] + sea_depth = sea_level - world.elevation['data'] for y in range(world.height): for x in range(world.width): if world.tiles_around((x, y), radius=1, predicate=world.is_land): - sea_depth[y][x] = 0 + sea_depth[y, x] = 0 elif world.tiles_around((x, y), radius=2, predicate=world.is_land): - sea_depth[y][x] *= 0.3 + sea_depth[y, x] *= 0.3 elif world.tiles_around((x, y), radius=3, predicate=world.is_land): - sea_depth[y][x] *= 0.5 + sea_depth[y, x] *= 0.5 elif world.tiles_around((x, y), radius=4, predicate=world.is_land): - sea_depth[y][x] *= 0.7 + sea_depth[y, x] *= 0.7 elif world.tiles_around((x, y), radius=5, predicate=world.is_land): - sea_depth[y][x] *= 0.9 + sea_depth[y, x] *= 0.9 sea_depth = anti_alias(sea_depth, 10) - min_depth, max_depth = matrix_min_and_max(sea_depth) - sea_depth = [[rescale_value(sea_depth[y][x], min_depth, - max_depth, 0.0, 1.0) - for x in range(world.width)] for y in - range(world.height)] + + min_depth = sea_depth.min() + max_depth = sea_depth.max() + sea_depth = (sea_depth - min_depth) / (max_depth - min_depth) return sea_depth diff --git a/worldengine/imex/__init__.py b/worldengine/imex/__init__.py index 10b759fe..7d08f1e1 100644 --- a/worldengine/imex/__init__.py +++ b/worldengine/imex/__init__.py @@ -12,24 +12,49 @@ import sys -def export(world, export_type, export_bpp, export_signed, export_normalize): +def export(world, export_filetype, export_bpp, export_signed, path = 'seed_output', export_raw = True): try: gdal except NameError: print("Cannot export: please install pygdal.") sys.exit(1) - final_driver = gdal.GetDriverByName(export_type) + final_driver = gdal.GetDriverByName(export_filetype) if final_driver is None: - print("%s driver not registered." % export_type) + print("%s driver not registered." % export_filetype) sys.exit(1) - if export_bpp == 8 and export_signed: - numpy_type = numpy.int8 - gdal_type = gdal.GDT_Byte + #check settings for some common filetypes + if export_filetype.lower() == 'png': + if export_signed != False or (export_bpp != 8 and export_bpp != 16): + print("A grayscale PNG only supports unsigned 8 or 16 Bit integers.") + export_raw = False + export_signed = False + if export_bpp <= 8: + export_bpp = 8 + else: + export_bpp = 16 + elif export_filetype.lower() == 'bmp': + if export_signed != False or export_bpp != 8: + print("GDAL only supports export to BMP as unsigned 8 Bit integers.") + export_raw = False + export_signed = False + export_bpp = 8 + + #process basic settings + if export_raw: + export_signed = True + export_bpp = 32 + numpy_type = numpy.float32 + gdal_type = gdal.GDT_Float32 + elif export_bpp == 8 and export_signed: + print("Export of signed 8BPP is not supported by GDAL. Switching to unsigned 8BPP.") + export_signed = False + numpy_type = numpy.uint8 + gdal_type = gdal.GDT_Byte#always unsigned! elif export_bpp == 8 and not export_signed: numpy_type = numpy.uint8 - gdal_type = gdal.GDT_Byte + gdal_type = gdal.GDT_Byte#always unsigned! elif export_bpp == 16 and export_signed: numpy_type = numpy.int16 gdal_type = gdal.GDT_Int16 @@ -47,30 +72,33 @@ def export(world, export_type, export_bpp, export_signed, export_normalize): sys.exit(1) # massage data to scale between the absolute min and max - elevation = numpy.array(world.elevation['data']) + elevation = numpy.copy(world.elevation['data']) - if not export_signed and elevation.min() < 0.0: - elevation += abs(elevation.min()) # TODO: need better way to handle negative numbers + if export_signed: + elevation = elevation - world.sea_level()#elevation 0 now refers to sea-level + else: + elevation -= elevation.min()#place the lowest point of the world at 0.0 - if export_normalize: + if not export_raw:#make best possible use of the available range; only available for integer exports if export_signed: - elevation *= (((2**export_bpp)/2)-1)/elevation.max() + elevation *= (2**(export_bpp - 1) - 1) / max(abs(elevation.min(), abs(elevation.max()))) else: - elevation *= (2**export_bpp)/elevation.max() + elevation *= (2**export_bpp - 1) / abs(elevation.max()) - elevation = elevation.astype(numpy_type) + elevation = elevation.astype(numpy_type)#no rounding performed # take elevation data and push it into an intermediate GTiff format inter_driver = gdal.GetDriverByName("GTiff") - _, inter_file = tempfile.mkstemp() - initial_ds = inter_driver.Create(inter_file, world.height, world.width, 1, gdal_type) + _, inter_file = tempfile.mkstemp()#returns (file-handle, absolute path) + initial_ds = inter_driver.Create(inter_file, world.width, world.height, 1, gdal_type) band = initial_ds.GetRasterBand(1) + band.WriteArray(elevation) band = None # dereference band initial_ds = None # save/flush and close # take the intermediate GTiff format and convert to final format initial_ds = gdal.Open(inter_file) - final_driver.CreateCopy('seed_output-%d.%s' % (export_bpp, export_type), initial_ds) + final_driver.CreateCopy('%s-%d.%s' % (path, export_bpp, export_filetype.lower()), initial_ds)#TODO: find a way to extract the proper file-suffix from the GDAL filetype os.remove(inter_file) diff --git a/worldengine/plates.py b/worldengine/plates.py index 7957d28d..ab63d266 100644 --- a/worldengine/plates.py +++ b/worldengine/plates.py @@ -4,6 +4,7 @@ import platec import random import time +import numpy from worldengine.generation import Step, add_noise_to_elevation, center_land, generate_world, \ get_verbose, initialize_ocean_and_thresholds, place_oceans_at_map_borders @@ -22,6 +23,7 @@ def generate_plates_simulation(seed, width, height, sea_level=0.65, p = platec.create(seed, width, height, sea_level, erosion_period, folding_ratio, aggr_overlap_abs, aggr_overlap_rel, cycle_count, num_plates) + # Note: To rescale the worlds heightmap to roughly Earths scale, multiply it by 2000. while platec.is_finished(p) == 0: # TODO: add a if verbose: message here? @@ -43,7 +45,7 @@ def _plates_simulation(name, width, height, seed, num_plates=10, verbose=verbose) world = World(name, width, height, seed, num_plates, ocean_level, step) - world.set_elevation(array_to_matrix(e_as_array, width, height), None) + world.set_elevation(numpy.array(e_as_array).reshape(height, width), None) world.set_plates(array_to_matrix(p_as_array, width, height)) return world diff --git a/worldengine/simulations/basic.py b/worldengine/simulations/basic.py index 547e1c0a..69080cc2 100644 --- a/worldengine/simulations/basic.py +++ b/worldengine/simulations/basic.py @@ -1,14 +1,21 @@ -def find_threshold(elevation, land_percentage, ocean=None): - width = len(elevation[0]) - height = len(elevation) +import numpy + +def find_threshold(map_data, land_percentage, ocean=None):#never used anywhere? + height, width = map_data.shape + + #maybe map was already masked when we got it; if not, this will make sure we operate on a mask + mask = numpy.ma.array(map_data, mask = False, keep_mask = True) + + if ocean is not None: + if ocean.shape != map_data.shape: + raise Exception( + "Dimension of map and ocean do not match. " + + "Map is %d x %d, while ocean is %d x%d" % ( + width, height, ocean.shape[1], ocean.shape[0])) + mask = numpy.ma.array(mask, mask = ocean, keep_mask = True) def count(e): - tot = 0 - for y in range(0, height): - for x in range(0, width): - if elevation[y][x] > e and (ocean is None or not ocean[y][x]): - tot += 1 - return tot + return numpy.ma.masked_less_equal(mask, e).count() def search(a, b, desired): if (not type(a) == int) or (not type(b) == int): @@ -31,38 +38,32 @@ def search(a, b, desired): else: return search(a, m, desired) - all_land = width * height - if ocean: - for y in range(0, height): - for x in range(0, width): - if ocean[y][x]: - all_land -= 1 + all_land = mask.count() desired_land = all_land * land_percentage return search(0, 255, desired_land) -def find_threshold_f(elevation, land_perc, ocean=None): - width = len(elevation[0]) - height = len(elevation) - if ocean: - if (width != len(ocean[0])) or (height != len(ocean)): +def find_threshold_f(map_data, land_perc, ocean=None, max=1000.0, mindist=0.005): + height, width = map_data.shape + + #maybe map was already masked when we got it; if not, this will make sure we operate on a mask + mask = numpy.ma.array(map_data, mask = False, keep_mask = True) + + if ocean is not None: + if ocean.shape != map_data.shape: raise Exception( - "Dimension of elevation and ocean do not match. " + - "Elevation is %d x %d, while ocean is %d x%d" % ( - width, height, len(ocean[0]), len(ocean))) + "Dimension of map_data and ocean do not match. " + + "Map is %d x %d, while ocean is %d x%d" % ( + width, height, ocean.shape[1], ocean.shape[0])) + mask = numpy.ma.array(mask, mask = ocean, keep_mask = True) def count(e): - tot = 0 - for y in range(0, height): - for x in range(0, width): - if elevation[y][x] > e and (ocean is None or not ocean[y][x]): - tot += 1 - return tot + return numpy.ma.masked_less_equal(mask, e).count() def search(a, b, desired): if a == b: return a - if abs(b - a) < 0.005: + if abs(b - a) < mindist: ca = count(a) cb = count(b) dista = abs(desired - ca) @@ -78,11 +79,6 @@ def search(a, b, desired): else: return search(a, m, desired) - all_land = width * height - if ocean: - for y in range(0, height): - for x in range(0, width): - if ocean[y][x]: - all_land -= 1 + all_land = mask.count() desired_land = all_land * land_perc - return search(-1000.0, 1000.0, desired_land) + return search(-1*max, max, desired_land) diff --git a/worldengine/simulations/biome.py b/worldengine/simulations/biome.py index 8ae87193..d4ca2063 100644 --- a/worldengine/simulations/biome.py +++ b/worldengine/simulations/biome.py @@ -1,3 +1,5 @@ +import numpy + class BiomeSimulation(object): @staticmethod @@ -14,101 +16,101 @@ def execute(world, seed): ocean = world.ocean cm = {} biome_cm = {} - biome = [[None for x in range(width)] for y in range(height)] # TODO: replace with numpy + biome = numpy.zeros((height, width), dtype = object)#this is still kind of expensive memory-wise for y in range(height): for x in range(width): - if ocean[y][x]: - biome[y][x] = 'ocean' + if ocean[y, x]: + biome[y, x] = 'ocean' else: if w.is_temperature_polar((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'polar desert' + biome[y, x] = 'polar desert' else: - biome[y][x] = 'ice' + biome[y, x] = 'ice' elif w.is_temperature_alpine((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'subpolar dry tundra' + biome[y, x] = 'subpolar dry tundra' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'subpolar moist tundra' + biome[y, x] = 'subpolar moist tundra' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'subpolar wet tundra' + biome[y, x] = 'subpolar wet tundra' else: - biome[y][x] = 'subpolar rain tundra' + biome[y, x] = 'subpolar rain tundra' elif w.is_temperature_boreal((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'boreal desert' + biome[y, x] = 'boreal desert' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'boreal dry scrub' + biome[y, x] = 'boreal dry scrub' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'boreal moist forest' + biome[y, x] = 'boreal moist forest' elif w.is_humidity_semiarid((x, y)): - biome[y][x] = 'boreal wet forest' + biome[y, x] = 'boreal wet forest' else: - biome[y][x] = 'boreal rain forest' + biome[y, x] = 'boreal rain forest' elif w.is_temperature_cool((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'cool temperate desert' + biome[y, x] = 'cool temperate desert' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'cool temperate desert scrub' + biome[y, x] = 'cool temperate desert scrub' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'cool temperate steppe' + biome[y, x] = 'cool temperate steppe' elif w.is_humidity_semiarid((x, y)): - biome[y][x] = 'cool temperate moist forest' + biome[y, x] = 'cool temperate moist forest' elif w.is_humidity_subhumid((x, y)): - biome[y][x] = 'cool temperate wet forest' + biome[y, x] = 'cool temperate wet forest' else: - biome[y][x] = 'cool temperate rain forest' + biome[y, x] = 'cool temperate rain forest' elif w.is_temperature_warm((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'warm temperate desert' + biome[y, x] = 'warm temperate desert' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'warm temperate desert scrub' + biome[y, x] = 'warm temperate desert scrub' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'warm temperate thorn scrub' + biome[y, x] = 'warm temperate thorn scrub' elif w.is_humidity_semiarid((x, y)): - biome[y][x] = 'warm temperate dry forest' + biome[y, x] = 'warm temperate dry forest' elif w.is_humidity_subhumid((x, y)): - biome[y][x] = 'warm temperate moist forest' + biome[y, x] = 'warm temperate moist forest' elif w.is_humidity_humid((x, y)): - biome[y][x] = 'warm temperate wet forest' + biome[y, x] = 'warm temperate wet forest' else: - biome[y][x] = 'warm temperate rain forest' + biome[y, x] = 'warm temperate rain forest' elif w.is_temperature_subtropical((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'subtropical desert' + biome[y, x] = 'subtropical desert' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'subtropical desert scrub' + biome[y, x] = 'subtropical desert scrub' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'subtropical thorn woodland' + biome[y, x] = 'subtropical thorn woodland' elif w.is_humidity_semiarid((x, y)): - biome[y][x] = 'subtropical dry forest' + biome[y, x] = 'subtropical dry forest' elif w.is_humidity_subhumid((x, y)): - biome[y][x] = 'subtropical moist forest' + biome[y, x] = 'subtropical moist forest' elif w.is_humidity_humid((x, y)): - biome[y][x] = 'subtropical wet forest' + biome[y, x] = 'subtropical wet forest' else: - biome[y][x] = 'subtropical rain forest' + biome[y, x] = 'subtropical rain forest' elif w.is_temperature_tropical((x, y)): if w.is_humidity_superarid((x, y)): - biome[y][x] = 'tropical desert' + biome[y, x] = 'tropical desert' elif w.is_humidity_perarid((x, y)): - biome[y][x] = 'tropical desert scrub' + biome[y, x] = 'tropical desert scrub' elif w.is_humidity_arid((x, y)): - biome[y][x] = 'tropical thorn woodland' + biome[y, x] = 'tropical thorn woodland' elif w.is_humidity_semiarid((x, y)): - biome[y][x] = 'tropical very dry forest' + biome[y, x] = 'tropical very dry forest' elif w.is_humidity_subhumid((x, y)): - biome[y][x] = 'tropical dry forest' + biome[y, x] = 'tropical dry forest' elif w.is_humidity_humid((x, y)): - biome[y][x] = 'tropical moist forest' + biome[y, x] = 'tropical moist forest' elif w.is_humidity_perhumid((x, y)): - biome[y][x] = 'tropical wet forest' + biome[y, x] = 'tropical wet forest' else: - biome[y][x] = 'tropical rain forest' + biome[y, x] = 'tropical rain forest' else: - biome[y][x] = 'bare rock' - if not biome[y][x] in biome_cm: - biome_cm[biome[y][x]] = 0 - biome_cm[biome[y][x]] += 1 + biome[y, x] = 'bare rock' + if not biome[y, x] in biome_cm: + biome_cm[biome[y, x]] = 0 + biome_cm[biome[y, x]] += 1 w.set_biome(biome) return cm, biome_cm diff --git a/worldengine/simulations/erosion.py b/worldengine/simulations/erosion.py index 3721d734..f73d7a20 100644 --- a/worldengine/simulations/erosion.py +++ b/worldengine/simulations/erosion.py @@ -66,7 +66,7 @@ def execute(self, world, seed): river_list.append(river) self.cleanUpFlow(river, world) rx, ry = river[-1] # find last cell in river - if not world.is_ocean((rx, ry)): + if not world.ocean[ry, rx]: lake_list.append(river[-1]) # river flowed into a lake # step four: simulate erosion and updating river map @@ -81,8 +81,8 @@ def execute(self, world, seed): lx, ly = lake lake_map[lx, ly] = 0.1 # TODO: make this based on rainfall/flow - world.set_rivermap(_numpy_to_matrix(river_map)) - world.set_lakemap(_numpy_to_matrix(lake_map)) + world.set_rivermap(river_map) + world.set_lakemap(lake_map) def find_water_flow(self, world, water_path): """Find the flow direction for each cell in heightmap""" @@ -111,7 +111,7 @@ def find_quick_path(self, river, world): # *** 1,2 *** x, y = river new_path = [] - lowest_elevation = world.elevation['data'][y][x] + lowest_elevation = world.elevation['data'][y, x] # lowestDirection = [0, 0] for dx, dy in DIR_NEIGHBORS: @@ -123,7 +123,7 @@ def find_quick_path(self, river, world): tx, ty = overflow(tx, world.width), overflow(ty, world.height) - elevation = world.elevation['data'][ty][tx] + elevation = world.elevation['data'][ty, tx] if elevation < lowest_elevation: if world.contains(temp_dir): @@ -151,7 +151,7 @@ def river_sources(world, water_flow, water_path): # above sea level are marked as 'sources'. for x in range(0, world.width - 1): for y in range(0, world.height - 1): - rain_fall = world.precipitation['data'][y][x] + rain_fall = world.precipitation['data'][y, x] water_flow[x, y] = rain_fall if water_path[x, y] == 0: @@ -216,7 +216,7 @@ def river_flow(self, source, world, river_list, lake_list): return path # skip the rest, return path # found a sea? - if world.is_ocean((x, y)): + if world.ocean[y, x]: break # find our immediate lowest elevation and flow there @@ -304,11 +304,11 @@ def cleanUpFlow(self, river, world): celevation = 1.0 for r in river: rx, ry = r - relevation = world.elevation['data'][ry][rx] + relevation = world.elevation['data'][ry, rx] if relevation <= celevation: celevation = relevation elif relevation > celevation: - world.elevation['data'][ry][rx] = celevation + world.elevation['data'][ry, rx] = celevation return river def findLowerElevation(self, source, world): @@ -317,7 +317,7 @@ def findLowerElevation(self, source, world): x, y = source currentRadius = 1 maxRadius = 40 - lowestElevation = world.elevation['data'][y][x] + lowestElevation = world.elevation['data'][y, x] destination = [] notFound = True isWrapped = False @@ -342,7 +342,7 @@ def findLowerElevation(self, source, world): # if utilities.outOfBounds([x+cx, y+cy], self.size): # print "Fixed:",x ,y, rx, ry - elevation = world.elevation['data'][ry][rx] + elevation = world.elevation['data'][ry, rx] # have we found a lower elevation? if elevation < lowestElevation: lowestElevation = elevation @@ -380,8 +380,8 @@ def river_erosion(self, river, world): continue if [x, y] in river: # ignore river itself continue - if world.elevation['data'][y][x] <= \ - world.elevation['data'][ry][ + if world.elevation['data'][y, x] <= \ + world.elevation['data'][ry, rx]: # ignore areas lower than river itself continue if not in_circle(radius, rx, ry, x, @@ -394,14 +394,14 @@ def river_erosion(self, river, world): elif adx == 2 or ady == 2: curve = 0.05 - diff = world.elevation['data'][ry][rx] - \ - world.elevation['data'][y][x] - newElevation = world.elevation['data'][y][x] + ( + diff = world.elevation['data'][ry, rx] - \ + world.elevation['data'][y, x] + newElevation = world.elevation['data'][y, x] + ( diff * curve) - if newElevation <= world.elevation['data'][ry][rx]: + if newElevation <= world.elevation['data'][ry, rx]: print('newElevation is <= than river, fix me...') - newElevation = world.elevation['data'][r][x] - world.elevation['data'][y][x] = newElevation + newElevation = world.elevation['data'][r, x] + world.elevation['data'][y, x] = newElevation return def rivermap_update(self, river, water_flow, rivermap, precipitations): @@ -415,5 +415,5 @@ def rivermap_update(self, river, water_flow, rivermap, precipitations): rivermap[x, y] = water_flow[x, y] isSeed = False else: - rivermap[x, y] = precipitations[y][x] + rivermap[px, py] + rivermap[x, y] = precipitations[y, x] + rivermap[px, py] px, py = x, y diff --git a/worldengine/simulations/humidity.py b/worldengine/simulations/humidity.py index 88fdda1a..e9401d7a 100644 --- a/worldengine/simulations/humidity.py +++ b/worldengine/simulations/humidity.py @@ -1,4 +1,5 @@ from worldengine.simulations.basic import find_threshold_f +import numpy class HumiditySimulation(object): @@ -17,12 +18,9 @@ def _calculate(world): temperatureWeight = 2.3 precipitationWeight = 1.0 irrigationWeight = 3 - humidity['data'] = [[0 for x in range(world.width)] for y in - range(world.height)] # TODO: replace with numpy + humidity['data'] = numpy.zeros((world.height, world.width), dtype=float) - for y in range(world.height): - for x in range(world.width): - humidity['data'][y][x] = (((world.precipitation['data'][y][x] * precipitationWeight - world.irrigation[y][x] * irrigationWeight)+1) * ((world.temperature_at((x, y)) * temperatureWeight + 1.0)/(temperatureWeight + 1.0))) + humidity['data'] = (((world.precipitation['data'] * precipitationWeight - world.irrigation * irrigationWeight) + 1) * ((world.temperature['data'] * temperatureWeight + 1) / (temperatureWeight + 1))) # These were originally evenly spaced at 12.5% each but changing them # to a bell curve produced better results diff --git a/worldengine/simulations/hydrology.py b/worldengine/simulations/hydrology.py index 916e8fb9..3f1ecc74 100644 --- a/worldengine/simulations/hydrology.py +++ b/worldengine/simulations/hydrology.py @@ -1,4 +1,5 @@ from worldengine.simulations.basic import find_threshold_f +import numpy class WatermapSimulation(object): @@ -17,15 +18,15 @@ def droplet(world, pos, q, _watermap): if q < 0: return x, y = pos - pos_elev = world.elevation['data'][y][x] + _watermap[y][x] + pos_elev = world.elevation['data'][y, x] + _watermap[y, x] lowers = [] min_higher = None min_lower = None # pos_min_higher = None # TODO: no longer used? tot_lowers = 0 - for p in world.tiles_around((x, y)): + for p in world.tiles_around((x, y)):#TODO: switch to numpy px, py = p - e = world.elevation['data'][py][px] + _watermap[py][px] + e = world.elevation['data'][py, px] + _watermap[py, px] if e < pos_elev: dq = int(pos_elev - e) << 2 if min_lower is None or e < min_lower: @@ -43,23 +44,22 @@ def droplet(world, pos, q, _watermap): f = q / tot_lowers for l in lowers: s, p = l - if world.is_land(p): + if not world.ocean[p[1], p[0]]: px, py = p ql = f * s # ql = q going = ql > 0.05 - _watermap[py][px] += ql + _watermap[py, px] += ql if going: droplet(world, p, ql, _watermap) else: - _watermap[y][x] += q + _watermap[y, x] += q - _watermap_data = [[0 for x in range(world.width)] for y in - range(world.height)] # TODO: replace with numpy + _watermap_data = numpy.zeros((world.height, world.width), dtype=float) for i in range(n): x, y = world.random_land() - if True and world.precipitation['data'][y][x] > 0: - droplet(world, (x, y), world.precipitation['data'][y][x], + if True and world.precipitation['data'][y, x] > 0: + droplet(world, (x, y), world.precipitation['data'][y, x], _watermap_data) _watermap = dict() _watermap['data'] = _watermap_data diff --git a/worldengine/simulations/irrigation.py b/worldengine/simulations/irrigation.py index 4dab1954..6c8bc4a1 100644 --- a/worldengine/simulations/irrigation.py +++ b/worldengine/simulations/irrigation.py @@ -5,7 +5,7 @@ class IrrigationSimulation(object): def is_applicable(world): return world.has_watermap() and (not world.has_irrigation()) - def execute(self, world, seed): + def execute(self, world, seed):#seed is currently not used world.irrigation = self._calculate(world) @staticmethod @@ -20,19 +20,19 @@ def _calculate(world): radius = 10 #create array of pre-calculated values -> less calculations - d = numpy.arange(-radius, radius + 1, 1, dtype=numpy.float) + d = numpy.arange(-radius, radius + 1, 1, dtype=float) x, y = numpy.meshgrid(d, d)#x/y distances to array center #calculate final matrix: ln(sqrt(x^2+y^2) + 1) + 1 logs = numpy.log1p(numpy.sqrt(numpy.square(x) + numpy.square(y))) + 1 #create output array - values = numpy.zeros((height, width), dtype=numpy.float) + values = numpy.zeros((height, width), dtype=float) it_all = numpy.nditer(values, flags=['multi_index'], op_flags=['readonly']) while not it_all.finished: x = it_all.multi_index[1] y = it_all.multi_index[0] - if world.is_land((x, y)): + if world.ocean[y, x]: #coordinates used for the values-slice (tl = top-left etc.) tl_v = (max(x - radius, 0) , max(y - radius, 0)) br_v = (min(x + radius, width - 1), min(y + radius, height - 1)) @@ -44,7 +44,7 @@ def _calculate(world): logs_relevant = logs[tl_l[1]:br_l[1]+1, tl_l[0]:br_l[0]+1] #finish calculation - values[tl_v[1]:br_v[1]+1, tl_v[0]:br_v[0]+1] += numpy.divide(world.watermap['data'][y][x], logs_relevant) + values[tl_v[1]:br_v[1]+1, tl_v[0]:br_v[0]+1] += world.watermap['data'][y, x] / logs_relevant it_all.iternext() diff --git a/worldengine/simulations/permeability.py b/worldengine/simulations/permeability.py index 62d979ac..8fbae206 100644 --- a/worldengine/simulations/permeability.py +++ b/worldengine/simulations/permeability.py @@ -1,5 +1,7 @@ from worldengine.simulations.basic import find_threshold_f +from noise import snoise2 import random +import numpy class PermeabilitySimulation(object): @@ -21,18 +23,15 @@ def execute(self, world, seed): def _calculate(seed, width, height): random.seed(seed * 37) base = random.randint(0, 4096) - perm = [[0 for x in range(width)] for y in range(height)] # TODO: replace with numpy - - from noise import snoise2 + perm = numpy.zeros((height, width), dtype=float) octaves = 6 freq = 64.0 * octaves - for y in range(0, height): + for y in range(0, height):#TODO: numpy optimization? # yscaled = float(y) / height # TODO: what is this? for x in range(0, width): n = snoise2(x / freq, y / freq, octaves, base=base) - t = n - perm[y][x] = t + perm[y, x] = n return perm diff --git a/worldengine/simulations/precipitation.py b/worldengine/simulations/precipitation.py index 9436d235..77215b01 100644 --- a/worldengine/simulations/precipitation.py +++ b/worldengine/simulations/precipitation.py @@ -1,5 +1,6 @@ import random import time +import numpy from noise import snoise2 from worldengine.simulations.basic import find_threshold_f @@ -34,13 +35,12 @@ def _calculate(seed, width, height): border = width / 4 random.seed(seed * 13) base = random.randint(0, 4096) - precipitations = [[0 for x in range(width)] for y in range(height)] - # TODO: replace with numpy + precipitations = numpy.zeros((height, width), dtype=float) octaves = 6 freq = 64.0 * octaves - for y in range(height): + for y in range(height):#TODO: numpy y_scaled = float(y) / height latitude_factor = 1.0 - (abs(y_scaled - 0.5) * 2) for x in range(width): @@ -54,6 +54,6 @@ def _calculate(seed, width, height): base=base) * (border - x) / border) precipitation = (latitude_factor + n * 4) / 5.0 - precipitations[y][x] = precipitation + precipitations[y, x] = precipitation return precipitations diff --git a/worldengine/simulations/temperature.py b/worldengine/simulations/temperature.py index 06f9bfb3..e2594787 100644 --- a/worldengine/simulations/temperature.py +++ b/worldengine/simulations/temperature.py @@ -1,5 +1,6 @@ from worldengine.simulations.basic import find_threshold_f import random +import numpy class TemperatureSimulation(object): @@ -32,7 +33,7 @@ def _calculate(world, seed, elevation, mountain_level): random.seed(seed * 7) base = random.randint(0, 4096) - temp = [[0 for x in range(width)] for y in range(height)] # TODO: replace with numpy + temp = numpy.zeros((height, width), dtype=float) from noise import snoise2 @@ -40,7 +41,7 @@ def _calculate(world, seed, elevation, mountain_level): octaves = 8 freq = 16.0 * octaves - for y in range(0, height): + for y in range(0, height):#TODO: Check for possible numpy optimizations. y_scaled = float(y) / height latitude_factor = 1.0 - (abs(y_scaled - 0.5) * 2) for x in range(0, width): @@ -54,13 +55,13 @@ def _calculate(world, seed, elevation, mountain_level): base=base) * (border - x) / border) t = (latitude_factor * 12 + n * 1) / 13.0 - if elevation[y][x] > mountain_level: - if elevation[y][x] > (mountain_level + 29): + if elevation[y, x] > mountain_level: + if elevation[y, x] > (mountain_level + 29): altitude_factor = 0.033 else: altitude_factor = 1.00 - ( - float(elevation[y][x] - mountain_level) / 30) + float(elevation[y, x] - mountain_level) / 30) t *= altitude_factor - temp[y][x] = t + temp[y, x] = t return temp diff --git a/worldengine/world.py b/worldengine/world.py index 58803052..01c4b93a 100644 --- a/worldengine/world.py +++ b/worldengine/world.py @@ -15,6 +15,7 @@ from worldengine.basic_map_operations import random_point import worldengine.protobuf.World_pb2 as Protobuf from worldengine.step import Step +from worldengine.common import _equal from worldengine.version import __version__ class World(object): @@ -37,17 +38,7 @@ def __init__(self, name, width, height, seed, num_plates, ocean_level, # def __eq__(self, other): - test = True - sd = self.__dict__ - od = other.__dict__ - for key, val in sd.items(): - if type(val) is numpy.ndarray: - test = numpy.array_equiv(val, od[key])#interesting alternative: numpy.allclose(a, b[, rtol, atol]) - else: - test = (val == od[key]) - if not test: - break - return test + return _equal(self.__dict__, other.__dict__) # # Serialization/Unserialization @@ -94,7 +85,16 @@ def _to_protobuf_matrix(matrix, p_matrix, transformation=None): for row in matrix: p_row = p_matrix.rows.add() for cell in row: - value = cell + ''' + When using numpy, certain primitive types are replaced with + numpy-specifc versions that, even though mostly compatible, + cannot be digested by protobuf. This might change at some point; + for now a conversion is necessary. + ''' + if type(cell) is numpy.bool_: + value = bool(cell) + else: + value = cell if transformation: value = transformation(value) p_row.cells.append(value) @@ -239,7 +239,7 @@ def _from_protobuf_world(cls, p_world): Step.get_by_name(p_world.generationData.step)) # Elevation - e = World._from_protobuf_matrix(p_world.heightMapData) + e = numpy.array(World._from_protobuf_matrix(p_world.heightMapData)) e_th = [('sea', p_world.heightMapTh_sea), ('plain', p_world.heightMapTh_plain), ('hill', p_world.heightMapTh_hill), @@ -250,26 +250,27 @@ def _from_protobuf_world(cls, p_world): w.set_plates(World._from_protobuf_matrix(p_world.plates)) # Ocean - w.set_ocean(World._from_protobuf_matrix(p_world.ocean)) - w.sea_depth = World._from_protobuf_matrix(p_world.sea_depth) + w.set_ocean(numpy.array(World._from_protobuf_matrix(p_world.ocean))) + w.sea_depth = numpy.array(World._from_protobuf_matrix(p_world.sea_depth)) # Biome if len(p_world.biome.rows) > 0: - w.set_biome( + w.set_biome(numpy.array( World._from_protobuf_matrix( - p_world.biome, biome_index_to_name)) + p_world.biome, biome_index_to_name), dtype = object)) # Humidity # FIXME: use setters if len(p_world.humidity.rows) > 0: w.humidity = World._from_protobuf_matrix_with_quantiles( p_world.humidity) + w.humidity['data'] = numpy.array(w.humidity['data'])#numpy conversion if len(p_world.irrigation.rows) > 0: w.irrigation = numpy.array(World._from_protobuf_matrix(p_world.irrigation)) if len(p_world.permeabilityData.rows) > 0: - p = World._from_protobuf_matrix(p_world.permeabilityData) + p = numpy.array(World._from_protobuf_matrix(p_world.permeabilityData)) p_th = [ ('low', p_world.permeability_low), ('med', p_world.permeability_med), @@ -279,15 +280,15 @@ def _from_protobuf_world(cls, p_world): if len(p_world.watermapData.rows) > 0: w.watermap = dict() - w.watermap['data'] = World._from_protobuf_matrix( - p_world.watermapData) + w.watermap['data'] = numpy.array(World._from_protobuf_matrix( + p_world.watermapData)) w.watermap['thresholds'] = {} w.watermap['thresholds']['creek'] = p_world.watermap_creek w.watermap['thresholds']['river'] = p_world.watermap_river w.watermap['thresholds']['main river'] = p_world.watermap_mainriver if len(p_world.precipitationData.rows) > 0: - p = World._from_protobuf_matrix(p_world.precipitationData) + p = numpy.array(World._from_protobuf_matrix(p_world.precipitationData)) p_th = [ ('low', p_world.precipitation_low), ('med', p_world.precipitation_med), @@ -296,7 +297,7 @@ def _from_protobuf_world(cls, p_world): w.set_precipitation(p, p_th) if len(p_world.temperatureData.rows) > 0: - t = World._from_protobuf_matrix(p_world.temperatureData) + t = numpy.array(World._from_protobuf_matrix(p_world.temperatureData)) t_th = [ ('polar', p_world.temperature_polar), ('alpine', p_world.temperature_alpine), @@ -309,11 +310,11 @@ def _from_protobuf_world(cls, p_world): w.set_temperature(t, t_th) if len(p_world.lakemap.rows) > 0: - m = World._from_protobuf_matrix(p_world.lakemap) + m = numpy.array(World._from_protobuf_matrix(p_world.lakemap)) w.set_lakemap(m) if len(p_world.rivermap.rows) > 0: - m = World._from_protobuf_matrix(p_world.rivermap) + m = numpy.array(World._from_protobuf_matrix(p_world.rivermap)) w.set_rivermap(m) return w @@ -323,8 +324,7 @@ def _from_protobuf_world(cls, p_world): # def contains(self, pos): - x, y = pos - return x >= 0 and y >= 0 and x < self.width and y < self.height + return (0 <= pos[0] < self.width) and (0 <= pos[1] < self.height) # # Land/Ocean @@ -332,18 +332,16 @@ def contains(self, pos): def random_land(self): x, y = random_point(self.width, self.height) - if self.ocean[y][x]: + if self.ocean[y, x]:#TODO: this method should get a safer/quicker way of finding land! return self.random_land() else: return x, y def is_land(self, pos): - x, y = pos - return not self.ocean[y][x] + return not self.ocean[pos[1], pos[0]]#faster than reversing pos or transposing ocean def is_ocean(self, pos): - x, y = pos - return self.ocean[y][x] + return self.ocean[pos[1], pos[0]] def sea_level(self): return self.elevation['thresholds'][0][1] @@ -378,10 +376,10 @@ def tiles_around(self, pos, radius=1, predicate=None): x, y = pos for dx in range(-radius, radius + 1): nx = x + dx - if nx >= 0 and nx < self.width: + if 0 <= nx < self.width: for dy in range(-radius, radius + 1): ny = y + dy - if ny >= 0 and ny < self.height and (dx != 0 or dy != 0): + if 0 <= ny < self.height and (dx != 0 or dy != 0): if predicate is None or predicate((nx, ny)): ps.append((nx, ny)) return ps @@ -415,26 +413,8 @@ def tiles_around_many(self, pos_list, radius=1, predicate=None): def start_mountain_th(self): return self.elevation['thresholds'][2][1] - def max_elevation(self): - max_el = None - for y in range(self.height): - for x in range(self.width): - el = self.elevation['data'][y][x] - if max_el is None or el > max_el: - max_el = el - return max_el - - def min_elevation(self): - min_el = None - for y in range(self.height): - for x in range(self.width): - el = self.elevation['data'][y][x] - if min_el is None or el < min_el: - min_el = el - return min_el - def is_mountain(self, pos): - if not self.is_land(pos): + if self.ocean[pos[1], pos[0]]: return False if len(self.elevation['thresholds']) == 4: mi = 2 @@ -453,10 +433,10 @@ def is_low_mountain(self, pos): mi = 1 mountain_level = self.elevation['thresholds'][mi][1] x, y = pos - return self.elevation['data'][y][x] < mountain_level + 2.0 + return self.elevation['data'][y, x] < mountain_level + 2.0 def level_of_mountain(self, pos): - if not self.is_land(pos): + if self.ocean[pos[1], pos[0]]: return False if len(self.elevation['thresholds']) == 4: mi = 2 @@ -464,10 +444,10 @@ def level_of_mountain(self, pos): mi = 1 mountain_level = self.elevation['thresholds'][mi][1] x, y = pos - if self.elevation['data'][y][x] <= mountain_level: + if self.elevation['data'][y, x] <= mountain_level: return 0 else: - return self.elevation['data'][y][x] - mountain_level + return self.elevation['data'][y, x] - mountain_level def is_high_mountain(self, pos): if not self.is_mountain(pos): @@ -478,10 +458,10 @@ def is_high_mountain(self, pos): mi = 1 mountain_level = self.elevation['thresholds'][mi][1] x, y = pos - return self.elevation['data'][y][x] > mountain_level + 4.0 + return self.elevation['data'][y, x] > mountain_level + 4.0 def is_hill(self, pos): - if not self.is_land(pos): + if self.ocean[pos[1], pos[0]]: return False if len(self.elevation['thresholds']) == 4: hi = 1 @@ -490,11 +470,10 @@ def is_hill(self, pos): hill_level = self.elevation['thresholds'][hi][1] mountain_level = self.elevation['thresholds'][hi + 1][1] x, y = pos - return hill_level < self.elevation['data'][y][x] < mountain_level + return hill_level < self.elevation['data'][y, x] < mountain_level def elevation_at(self, pos): - x, y = pos - return self.elevation['data'][y][x] + return self.elevation['data'][pos[1], pos[0]] # # Precipitations @@ -502,7 +481,7 @@ def elevation_at(self, pos): def precipitations_at(self, pos): x, y = pos - return self.precipitation['data'][y][x] + return self.precipitation['data'][y, x] def precipitations_thresholds(self): return self.precipitation['thresholds'] @@ -514,53 +493,53 @@ def precipitations_thresholds(self): def is_temperature_polar(self, pos): th_max = self.temperature['thresholds'][0][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return t < th_max def is_temperature_alpine(self, pos): th_min = self.temperature['thresholds'][0][1] th_max = self.temperature['thresholds'][1][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return th_max > t >= th_min def is_temperature_boreal(self, pos): th_min = self.temperature['thresholds'][1][1] th_max = self.temperature['thresholds'][2][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return th_max > t >= th_min def is_temperature_cool(self, pos): th_min = self.temperature['thresholds'][2][1] th_max = self.temperature['thresholds'][3][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return th_max > t >= th_min def is_temperature_warm(self, pos): th_min = self.temperature['thresholds'][3][1] th_max = self.temperature['thresholds'][4][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return th_max > t >= th_min def is_temperature_subtropical(self, pos): th_min = self.temperature['thresholds'][4][1] th_max = self.temperature['thresholds'][5][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return th_max > t >= th_min def is_temperature_tropical(self, pos): th_min = self.temperature['thresholds'][5][1] x, y = pos - t = self.temperature['data'][y][x] + t = self.temperature['data'][y, x] return t >= th_min def temperature_at(self, pos): x, y = pos - return self.temperature['data'][y][x] + return self.temperature['data'][y, x] def temperature_thresholds(self): return self.temperature['thresholds'] @@ -572,61 +551,61 @@ def temperature_thresholds(self): def is_humidity_above_quantile(self, pos, q): th = self.humidity['quantiles'][str(q)] x, y = pos - v = self.humidity['data'][y][x] + v = self.humidity['data'][y, x] return v >= th def is_humidity_superarid(self, pos): th_max = self.humidity['quantiles']['87'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return t < th_max def is_humidity_perarid(self, pos): th_min = self.humidity['quantiles']['87'] th_max = self.humidity['quantiles']['75'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_arid(self, pos): th_min = self.humidity['quantiles']['75'] th_max = self.humidity['quantiles']['62'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_semiarid(self, pos): th_min = self.humidity['quantiles']['62'] th_max = self.humidity['quantiles']['50'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_subhumid(self, pos): th_min = self.humidity['quantiles']['50'] th_max = self.humidity['quantiles']['37'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_humid(self, pos): th_min = self.humidity['quantiles']['37'] th_max = self.humidity['quantiles']['25'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_perhumid(self, pos): th_min = self.humidity['quantiles']['25'] th_max = self.humidity['quantiles']['12'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return th_max > t >= th_min def is_humidity_superhumid(self, pos): th_min = self.humidity['quantiles']['12'] x, y = pos - t = self.humidity['data'][y][x] + t = self.humidity['data'][y, x] return t >= th_min # @@ -639,24 +618,24 @@ def contains_stream(self, pos): def contains_creek(self, pos): x, y = pos - v = self.watermap['data'][y][x] + v = self.watermap['data'][y, x] return self.watermap['thresholds']['creek'] <= v < \ self.watermap['thresholds']['river'] def contains_river(self, pos): x, y = pos - v = self.watermap['data'][y][x] + v = self.watermap['data'][y, x] return self.watermap['thresholds']['river'] <= v < \ self.watermap['thresholds']['main river'] def contains_main_river(self, pos): x, y = pos - v = self.watermap['data'][y][x] + v = self.watermap['data'][y, x] return v >= self.watermap['thresholds']['main river'] def watermap_at(self, pos): x, y = pos - return self.watermap['data'][y][x] + return self.watermap['data'][y, x] # # Biome @@ -664,7 +643,7 @@ def watermap_at(self, pos): def biome_at(self, pos): x, y = pos - b = Biome.by_name(self.biome[y][x]) + b = Biome.by_name(self.biome[y, x]) if b is None: raise Exception('Not found') return b @@ -819,11 +798,11 @@ def n_actual_plates(self): # def set_elevation(self, data, thresholds): - if (len(data) != self.height) or (len(data[0]) != self.width): + if data.shape != (self.height, self.width): raise Exception( "Setting elevation map with wrong dimension. " "Expected %d x %d, found %d x %d" % ( - self.width, self.height, len(data[0]), len(data))) + self.width, self.height, data.shape[1], data.shape[0])) self.elevation = {'data': data, 'thresholds': thresholds} def set_plates(self, data): @@ -835,47 +814,47 @@ def set_plates(self, data): self.plates = data def set_biome(self, biome): - if len(biome) != self.height: + if biome.shape[0] != self.height: raise Exception( "Setting data with wrong height: biome has height %i while " "the height is currently %i" % ( - len(biome), self.height)) - if len(biome[0]) != self.width: + biome.shape[0], self.height)) + if biome.shape[1] != self.width: raise Exception("Setting data with wrong width") self.biome = biome def set_ocean(self, ocean): - if (len(ocean) != self.height) or (len(ocean[0]) != self.width): + if (ocean.shape[0] != self.height) or (ocean.shape[1] != self.width): raise Exception( "Setting ocean map with wrong dimension. Expected %d x %d, " "found %d x %d" % (self.width, self.height, - len(ocean[0]), len(ocean))) + ocean.shape[1], ocean.shape[0])) self.ocean = ocean def set_precipitation(self, data, thresholds): """"Precipitation is a value in [-1,1]""" - if len(data) != self.height: + if data.shape[0] != self.height: raise Exception("Setting data with wrong height") - if len(data[0]) != self.width: + if data.shape[1] != self.width: raise Exception("Setting data with wrong width") self.precipitation = {'data': data, 'thresholds': thresholds} def set_temperature(self, data, thresholds): - if len(data) != self.height: + if data.shape[0] != self.height: raise Exception("Setting data with wrong height") - if len(data[0]) != self.width: + if data.shape[1] != self.width: raise Exception("Setting data with wrong width") self.temperature = {'data': data, 'thresholds': thresholds} def set_permeability(self, data, thresholds): - if len(data) != self.height: + if data.shape[0] != self.height: raise Exception("Setting data with wrong height") - if len(data[0]) != self.width: + if data.shape[1] != self.width: raise Exception("Setting data with wrong width") self.permeability = {'data': data, 'thresholds': thresholds}