From e55497b1cde0d558d7b9b831984550c65edcfdde Mon Sep 17 00:00:00 2001 From: Emi Simpson Date: Fri, 10 Feb 2023 19:02:14 -0500 Subject: [PATCH] Add ability to load data from terrain map --- read_in.py | 121 ++++++++++++++++++++++++++++++++++++++++++++++++----- shared.py | 30 +++++++++++++ 2 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 shared.py diff --git a/read_in.py b/read_in.py index f019400..09c223f 100644 --- a/read_in.py +++ b/read_in.py @@ -1,7 +1,11 @@ from emis_funky_funktions import * +from shared import Terrain + +from PIL import Image + from itertools import chain -from typing import List, NamedTuple +from typing import List, NamedTuple, Tuple def meters_to_millimeters(meters: float) -> int: """ @@ -69,8 +73,8 @@ def read_elevation_line(line: str) -> Result[Iterator[int], Tuple[int, str]]: for (col_no, elem) in enumerate(line.split()[:-5]) ]) -class ErrorLocation(NamedTuple): - "The location of an error within file" +class UnparsableElevation(NamedTuple): + "An elevation entry failed to be parsed as a floating point" line_no: int "The line on which the error occurred" @@ -81,12 +85,12 @@ class ErrorLocation(NamedTuple): invalid_entry: str "The text which failed to parse as a floating point value" -def read_elevations(lines: str) -> Result[Iterator[int], ErrorLocation]: +def read_elevations(lines: Iterable[str]) -> Result[Iterator[int], UnparsableElevation]: """ Read an entire elevation file into a list This parses each line in of the file using `read_elevation_line()`. Errors - encountered are reported using an `ErrorLocation`. The resulting list is + encountered are reported using an `UnparsableElevation`. The resulting list is single-dimensional, but can be indexed arbitrarily, meaning for an input file with a constant number of columns per line, this data can be accessed as if it were a 2d square array. @@ -98,27 +102,124 @@ def read_elevations(lines: str) -> Result[Iterator[int], ErrorLocation]: ... 1 2 3 4 5 6 7 8 9e 10 ... 11 2.0 3e1 4 5 6 7 bwa 9 10 ... 11 2.0 3e0 4 5 ign err 8 9 10 - ... ''')) + ... '''.strip().split('\\n'))) Ok([1000, 2000, 3000, 4000, 5000, 11000, 2000, 30000, 4000, 5000, 11000, 2000, 3000, 4000, 5000]) >>> map_res(list, read_elevations(''' ... 1 2 3 4 5 6 7 8 9e 10 ... 11 ERR 3e1 4 5 6 7 bwa 9 10 ... 11 2.0 3e0 4 5 ign err 8 9 10 - ... ''')) - Err(ErrorLocation(line_no=2, col_no=2, invalid_entry='ERR')) + ... '''.strip().split('\\n'))) + Err(UnparsableElevation(line_no=2, col_no=2, invalid_entry='ERR')) """ return map_res( chain.from_iterable, sequence([ map_err( - lambda err: ErrorLocation(line_no + 1, *err), + lambda err: UnparsableElevation(line_no + 1, *err), read_elevation_line(line) ) - for (line_no, line) in enumerate(lines.strip().split('\n')) + for (line_no, line) in enumerate(lines) ]) ) +def gb_to_terrain_type(green_blue: Tuple[int, int]) -> Option[Terrain]: + """ + Maps the green and blue components of a color to a terrain type. + + If no terrain type exists with that color, then `None` is returned. + + >>> gb_to_terrain_type((192, 0)) + Some() + + >>> gb_to_terrain_type((192, 1)) is None + True + """ + match green_blue: + case (148, 18): + return Some(Terrain.OPEN_LAND) + case (192, 0): + return Some(Terrain.ROUGH_MEADOW) + case (255, 255): + return Some(Terrain.EASY_FOREST) + case (208, 60): + return Some(Terrain.MEDIUM_FOREST) + case (136, 40): + return Some(Terrain.WALK_FOREST) + case (73, 24): + return Some(Terrain.BRUSH) + case (0, 255): + return Some(Terrain.WET) + case (51, 3): + return Some(Terrain.ROAD) + case (0, 0): + return Some(Terrain.FOOTPATH) + case (0, 101): + return Some(Terrain.OOB) + case _: + return None + +class UnrecognizedColor(NamedTuple): + "A color in a provided map was not in the list of recognized colors" + + x: int + "The x coordinate of the unidentifided color" + + y: int + "The y coordinate of the unidentifided color" + +def load_gb_pixels_as_map(pixels: Iterable[Tuple[int, int]]) -> Result[Iterator[Terrain], UnrecognizedColor]: + """ + Interpret the green and blue bands of an image as pixels on a map + + This assumes that the map follows the specification laid out at + https://cs.rit.edu/~jro/courses/intelSys/labs/orienteering/ + + Namely, the map should be exactly 395 pixels wide, and should follow the color + conventions laid out in the Terrain Type table. + + This function is only interested in the green and blue channels of the image. Rather + than pass a full pixel, it is only necessary to pass the green and blue color values. + + If an error is encountered, the returned `Err` type will contain the x and y + coordinates of the pixel which could not be identified. + """ + return sequence([ + note( + UnrecognizedColor(pix_no % 395, pix_no // 395), + gb_to_terrain_type(gb) + ) + for (pix_no, gb) in enumerate(pixels) + ]) + +def load_image(path: str) -> Result[List[Terrain], UnrecognizedColor]: + "Run `load_gb_pixels_as_map` on an image read from a path" + with Image.open(path) as im: + return load_gb_pixels_as_map(zip(im.getdata(1), im.getdata(2))) + +def load_elevations(path: str) -> Result[Iterator[int], UnparsableElevation]: + "Run `read_elevations` on a text file read from a path" + with open(path) as file: + return read_elevations(file) + +def load_world_from_paths(map_path: str, elevation_path: str) -> Result[Iterable[Tuple[Terrain, int]], UnparsableElevation | UnrecognizedColor]: + """ + Read world information (terrain and elevation) from file paths. + + See `read_elevations` and `load_gb_pixels_as_map` for more information on the format + that this data should be in. Note that for this method, you do not need to worry + about the channels present in the terrain map, just that it align with the colors and + size of the specified format. + """ + return bind_res( + lambda terrain_data: + map_res( + p(zip, terrain_data), + load_elevations(elevation_path) + ), + load_image(map_path) + ) + if __name__ == '__main__': import doctest doctest.testmod() \ No newline at end of file diff --git a/shared.py b/shared.py new file mode 100644 index 0000000..9853ca8 --- /dev/null +++ b/shared.py @@ -0,0 +1,30 @@ +from emis_funky_funktions import * + +from enum import Enum, unique + +@unique +class Terrain(int, Enum): + """ + Various types of terrain understandable by the routing algorithm. + + Each element of the terrain is associated with an integer value indictating the cost + that that terrain has on movement speed. Units are (approximately) measured in + seconds / kilometer. + + For reference, a typical jogging speed is roughly 500 s/km, and a brisk walking + speed is about 700 s/km. + """ + OPEN_LAND = 510 + ROUGH_MEADOW = 700 + EASY_FOREST = 530 + MEDIUM_FOREST = 666 + WALK_FOREST = 800 + BRUSH = 24000 + WET = 6000 + ROAD = 500 + FOOTPATH = 505 + OOB = 2 ** 62 + +if __name__ == '__main__': + import doctest + doctest.testmod() \ No newline at end of file