Add ability to load data from terrain map

This commit is contained in:
Emi Simpson 2023-02-10 19:02:14 -05:00
parent bfd93bd55b
commit e55497b1cd
Signed by: Emi
GPG Key ID: A12F2C2FFDC3D847
2 changed files with 141 additions and 10 deletions

View File

@ -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(<Terrain.ROUGH_MEADOW: 700>)
>>> 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()

30
shared.py Normal file
View File

@ -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()