Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Changes from STL #58

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions openlr_dereferencer/decoding/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,20 +22,19 @@
)
from .configuration import Config, DEFAULT_CONFIG, load_config, save_config

LR = TypeVar("LocationReference",
LineLocationReference,
PointAlongLineLocationReference,
PoiWithAccessPointLocationReference,
GeoCoordinateLocationReference)
LR = TypeVar(
"LocationReference",
LineLocationReference,
PointAlongLineLocationReference,
PoiWithAccessPointLocationReference,
GeoCoordinateLocationReference,
)

MapObjects = TypeVar("MapObjects", LineLocation, Coordinates, PointAlongLine)


def decode(
reference: LR,
reader: MapReader,
observer: Optional[DecoderObserver] = None,
config: Config = DEFAULT_CONFIG
reference: LR, reader: MapReader, observer: Optional[DecoderObserver] = None, config: Config = DEFAULT_CONFIG
) -> MapObjects:
"""Translates an openLocationReference into a real location on your map.

Expand Down
95 changes: 58 additions & 37 deletions openlr_dereferencer/decoding/candidate_functions.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
"Contains functions for candidate searching and map matching"

import functools
from itertools import product
from logging import debug
from typing import Optional, Iterable, List, Tuple
import time
from openlr import FRC, LocationReferencePoint
from ..maps import shortest_path, MapReader, Line, Node
from ..maps.a_star import LRPathNotFoundError
from ..observer import DecoderObserver
from .candidate import Candidate
from .scoring import score_lrp_candidate, angle_difference
from .error import LRDecodeError
from .error import LRDecodeError, LRTimeoutError
from .path_math import coords, project, compute_bearing
from .routes import Route
from .configuration import Config
Expand All @@ -23,8 +24,9 @@ def make_candidate(
# we don't need to project on the point that is the degenerated line.
if line.geometry.length == 0:
return
point_on_line = project(line, coords(lrp))
point_on_line = project(line, coords(lrp), config.equal_area)
reloff = point_on_line.relative_offset

# In case the LRP is not the last LRP
if not is_last_lrp:
# Snap to the relevant end of the line, only if the node is not a simple connection node between two lines:
Expand Down Expand Up @@ -52,55 +54,58 @@ def make_candidate(
# Drop candidate if there is no partial line left
if is_last_lrp and reloff <= 0.0 or not is_last_lrp and reloff >= 1.0:
return
candidate = Candidate(line, reloff)
bearing = compute_bearing(lrp, candidate, is_last_lrp, config.bear_dist)
candidate = Candidate(line, reloff, config.equal_area)
bearing = compute_bearing(lrp, candidate, is_last_lrp, config.bear_dist, config.equal_area)
bear_diff = angle_difference(bearing, lrp.bear)
if abs(bear_diff) > config.max_bear_deviation:
if observer is not None:
observer.on_candidate_rejected(
lrp, candidate,
lrp,
candidate,
f"Bearing difference = {bear_diff} greater than max. bearing deviation = {config.max_bear_deviation}",
)
debug(
f"Not considering {candidate} because the bearing difference is {bear_diff} °.",
f"bear: {bearing}. lrp bear: {lrp.bear}",
f"Not considering {candidate} because the bearing difference is {bear_diff} °."
+ f"bear: {bearing}. lrp bear: {lrp.bear}",
)
return
candidate.score = score_lrp_candidate(lrp, candidate, config, is_last_lrp)
if candidate.score < config.min_score:
if observer is not None:
observer.on_candidate_rejected(
lrp, candidate,
lrp,
candidate,
f"Candidate score = {candidate.score} lower than min. score = {config.min_score}",
)
debug(
f"Not considering {candidate}",
f"Candidate score = {candidate.score} < min. score = {config.min_score}",
f"Not considering {candidate}" + f"Candidate score = {candidate.score} < min. score = {config.min_score}",
)
return
if observer is not None:
observer.on_candidate_found(
lrp, candidate,
lrp,
candidate,
)
return candidate


def nominate_candidates(
lrp: LocationReferencePoint, reader: MapReader, config: Config,
observer: Optional[DecoderObserver], is_last_lrp: bool
lrp: LocationReferencePoint,
reader: MapReader,
config: Config,
observer: Optional[DecoderObserver],
is_last_lrp: bool,
) -> Iterable[Candidate]:
"Yields candidate lines for the LRP along with their score."
debug(
f"Finding candidates for LRP {lrp} at {coords(lrp)} in radius {config.search_radius}"
)
debug(f"Finding candidates for LRP {lrp} at {coords(lrp)} in radius {config.search_radius}")
for line in reader.find_lines_close_to(coords(lrp), config.search_radius):
candidate = make_candidate(lrp, line, config, observer, is_last_lrp)
if candidate:
yield candidate


def get_candidate_route(
start: Candidate, dest: Candidate, lfrc: FRC, maxlen: float
start: Candidate, dest: Candidate, lfrc: FRC, maxlen: float, equal_area: bool
) -> Optional[Route]:
"""Returns the shortest path between two LRP candidates, excluding partial lines.

Expand All @@ -126,18 +131,19 @@ def get_candidate_route(
debug(f"Try to find path between {start, dest}")
if start.line.line_id == dest.line.line_id:
return Route(start, [], dest)
debug(
f"Finding path between nodes {start.line.end_node.node_id, dest.line.start_node.node_id}"
)
linefilter = lambda line: line.frc <= lfrc
debug(f"Finding path between nodes {start.line.end_node.node_id, dest.line.start_node.node_id}")

def linefilter(line, lfrc=lfrc):
return line.frc <= lfrc

try:
path = shortest_path(
start.line.end_node, dest.line.start_node, linefilter, maxlen=maxlen
start.line.end_node, dest.line.start_node, linefilter, maxlen=maxlen, equal_area=equal_area
)
debug(f"Returning {path}")
return Route(start, path, dest)
except LRPathNotFoundError:
debug(f"No path found between these nodes")
debug(f"No path found between these nodes: ({start.line.end_node.node_id}, {dest.line.start_node.node_id})")
return None


Expand All @@ -148,6 +154,7 @@ def match_tail(
reader: MapReader,
config: Config,
observer: Optional[DecoderObserver],
start_time: Optional[float] = None,
) -> List[Route]:
"""Searches for the rest of the line location.

Expand All @@ -169,6 +176,9 @@ def match_tail(
The wanted behaviour, as configuration options
observer:
The optional decoder observer, which emits events and calls back.
elapsed_time:
Time in seconds since outer-most call to `match_tail()` was initiated.


Returns:
If any candidate pair matches, the function calls itself for the rest of `tail` and
Expand All @@ -177,8 +187,17 @@ def match_tail(
Raises:
LRDecodeError:
If no candidate pair matches or a recursive call can not resolve a route.
LRTimeoutError:
If `elapsed_time` > `config.timeout` before a candidate pair is matched
"""
if start_time is None:
start_time = time.time()
elapsed_time = time.time() - start_time
if elapsed_time > config.timeout:
raise LRTimeoutError("Decoding was unsuccessful: timed out trying to find a match.")

last_lrp = len(tail) == 1

# The accepted distance to next point. This helps to save computations and filter bad paths
minlen = (1 - config.max_dnp_deviation) * current.dnp - config.tolerated_dnp_dev
maxlen = (1 + config.max_dnp_deviation) * current.dnp + config.tolerated_dnp_dev
Expand All @@ -191,20 +210,17 @@ def match_tail(
pairs = list(product(candidates, next_candidates))
# Sort by line scores
pairs.sort(key=lambda pair: (pair[0].score + pair[1].score), reverse=True)

# For every pair of candidates, search for a path matching our requirements
for (c_from, c_to) in pairs:
for c_from, c_to in pairs:
route = handleCandidatePair(
(current, next_lrp), (c_from, c_to), observer, lfrc, minlen, maxlen
(current, next_lrp), (c_from, c_to), observer, lfrc, minlen, maxlen, config.equal_area
)
if route is None:
continue
if last_lrp:
return [route]
try:
return [route] + match_tail(
next_lrp, [c_to], tail[1:], reader, config, observer
)
return [route] + match_tail(next_lrp, [c_to], tail[1:], reader, config, observer, start_time)
except LRDecodeError:
debug("Recursive call to resolve remaining path had no success")
continue
Expand All @@ -221,6 +237,7 @@ def handleCandidatePair(
lowest_frc: FRC,
minlen: float,
maxlen: float,
equal_area: bool,
) -> Optional[Route]:
"""
Try to find an adequate route between two LRP candidates.
Expand All @@ -245,7 +262,7 @@ def handleCandidatePair(
"""
current, next_lrp = lrps
source, dest = candidates
route = get_candidate_route(source, dest, lowest_frc, maxlen)
route = get_candidate_route(source, dest, lowest_frc, maxlen, equal_area)

if not route:
debug("No path for candidate found")
Expand All @@ -271,11 +288,13 @@ def handleCandidatePair(
return route


@functools.lru_cache(maxsize=1000)
def is_valid_node(node: Node):
"""
Checks if a node is a valid node. A valid node is a node that corresponds to a real-world junction
"""
return not is_invalid_node(node)
v = not is_invalid_node(node)
return v


def is_invalid_node(node: Node):
Expand All @@ -284,21 +303,23 @@ def is_invalid_node(node: Node):
"""

# Get a list of all incoming lines to the node
incoming_lines = list(node.incoming_lines())
incoming_line_nodes = list(node.incoming_line_nodes())

# Get a list of all outgoing lines from the node
outgoing_lines = list(node.outgoing_lines())
outgoing_line_nodes = list(node.outgoing_line_nodes())

# Check the number of incoming and outgoing lines
if (len(incoming_lines) == 1 and len(outgoing_lines) == 1) or (len(incoming_lines) == 2 and len(outgoing_lines) == 2):
if (len(incoming_line_nodes) == 1 and len(outgoing_line_nodes) == 1) or (
len(incoming_line_nodes) == 2 and len(outgoing_line_nodes) == 2
):
# Get the unique nodes of all incoming and outgoing lines
unique_nodes = set()

for line in incoming_lines:
for line in outgoing_line_nodes:
unique_nodes.add(line.start_node)
unique_nodes.add(line.end_node)

for line in outgoing_lines:
for line in outgoing_line_nodes:
unique_nodes.add(line.start_node)
unique_nodes.add(line.end_node)

Expand Down
36 changes: 20 additions & 16 deletions openlr_dereferencer/decoding/configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,11 @@ class Config(NamedTuple):
fow_standin_score: List[List[float]] = DEFAULT_FOW_STAND_IN_SCORE
#: The bearing angle is computed along this distance on a given line. Given in meters.
bear_dist: int = 20
#: Input coordinates are provided in an equal-area projection (i.e. NOT WGS84 lat/lon)
equal_area: bool = False
equal_area_srid: int = 9311
#: Timeout in seconds for single segment line decoding
timeout: int = 50000


DEFAULT_CONFIG = Config()
Expand Down Expand Up @@ -101,22 +106,21 @@ def load_config(source: Union[str, TextIOBase, dict]) -> Config:
raise TypeError("Surprising type")
return Config._make(
[
opened_source['search_radius'],
opened_source['max_dnp_deviation'],
opened_source['tolerated_dnp_dev'],
opened_source['min_score'],
{
FRC(int(key)): FRC(value)
for (key, value) in opened_source['tolerated_lfrc'].items()
},
opened_source['candidate_threshold'],
opened_source['max_bear_deviation'],
opened_source['fow_weight'],
opened_source['frc_weight'],
opened_source['geo_weight'],
opened_source['bear_weight'],
opened_source['fow_standin_score'],
opened_source['bear_dist']
opened_source["search_radius"],
opened_source["max_dnp_deviation"],
opened_source["tolerated_dnp_dev"],
opened_source["min_score"],
{FRC(int(key)): FRC(value) for (key, value) in opened_source["tolerated_lfrc"].items()},
opened_source["candidate_threshold"],
opened_source["max_bear_deviation"],
opened_source["fow_weight"],
opened_source["frc_weight"],
opened_source["geo_weight"],
opened_source["bear_weight"],
opened_source["fow_standin_score"],
opened_source["bear_dist"],
opened_source["equal_area"],
opened_source["timeout"],
]
)

Expand Down
5 changes: 4 additions & 1 deletion openlr_dereferencer/decoding/error.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
class LRDecodeError(Exception):
"An error that happens through decoding location references"
"An error that happens through decoding location references"

class LRTimeoutError(Exception):
"A timeout error that happens through decoding location references"
13 changes: 5 additions & 8 deletions openlr_dereferencer/decoding/line_decoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,23 +11,20 @@


def dereference_path(
lrps: List[LocationReferencePoint],
reader: MapReader,
config: Config,
observer: Optional[DecoderObserver]
lrps: List[LocationReferencePoint], reader: MapReader, config: Config, observer: Optional[DecoderObserver]
) -> List[Route]:
"Decode the location reference path, without considering any offsets"
first_lrp = lrps[0]
first_candidates = list(nominate_candidates(first_lrp, reader, config, observer, False))

linelocationpath = match_tail(first_lrp, first_candidates, lrps[1:], reader, config, observer)
return linelocationpath


def decode_line(reference: LineLocationReference, reader: MapReader, config: Config,
observer: Optional[DecoderObserver]) -> LineLocation:
def decode_line(
reference: LineLocationReference, reader: MapReader, config: Config, observer: Optional[DecoderObserver]
) -> LineLocation:
"""Decodes an openLR line location reference

Candidates are searched in a radius of `radius` meters around an LRP."""
parts = dereference_path(reference.points, reader, config, observer)
return build_line_location(parts, reference)
return build_line_location(parts, reference, config.equal_area)
Loading