Add ability to load data from terrain map
This commit is contained in:
parent
bfd93bd55b
commit
e55497b1cd
121
read_in.py
121
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(<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
30
shared.py
Normal 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()
|
Loading…
Reference in a new issue