mapd: New implementation and remove deprecated libraries

This commit is contained in:
DevTekVE
2023-12-19 22:14:06 +00:00
committed by Jason Wen
parent 7cdb6bb45d
commit 57dfee47b8
53 changed files with 1151 additions and 26343 deletions
+12
View File
@@ -5,6 +5,18 @@ sunnypilot - 0.9.6.1 (2023-xx-xx)
* UPDATED: Vision-based Turn Speed Control (V-TSC) implementation
* Refactored implementation thanks to pfeiferj!
* More accurate and consistent velocity calculation to achieve smoother longitudinal control in curves
* UPDATED: OpenStreetMap (OSM) implementation
* Refactored implementation thanks to pfeiferj!
* Less resource impact
* Significantly smaller sizes with databases
* All regions are available to download
* Weekly map updates thanks to pfeiferj!
* Increased the font size of the road name
* C3X-specific changes
* Altitude (ALT.) display on Developer UI
* Current street name on top of driving screen when "OSM Debug UI" is enabled
* DISABLED: Map-based Turn Speed Control (M-TSC)
* Reimplementation in near future updates
* UI updates
* RE-ENABLED: Navigation: Full screen support
* Display the map view in full screen
+16 -2
View File
@@ -178,6 +178,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"Offroad_TemperatureTooHigh", CLEAR_ON_MANAGER_START},
{"Offroad_UnofficialHardware", CLEAR_ON_MANAGER_START},
{"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START},
{"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START},
{"OpenpilotEnabledToggle", PERSISTENT},
{"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
{"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION},
@@ -274,9 +275,7 @@ std::unordered_map<std::string, uint32_t> keys = {
{"OnroadScreenOff", PERSISTENT},
{"OnroadScreenOffBrightness", PERSISTENT},
{"OnroadScreenOffEvent", PERSISTENT},
{"OsmDbUpdatesCheck", PERSISTENT},
{"OsmLocal", PERSISTENT},
{"OsmLocalDb", PERSISTENT},
{"OsmLocationName", PERSISTENT},
{"OsmLocationTitle", PERSISTENT},
{"OsmLocationUrl", PERSISTENT},
@@ -310,6 +309,21 @@ std::unordered_map<std::string, uint32_t> keys = {
{"VwAccType", PERSISTENT},
{"VwCCOnly", PERSISTENT},
{"Offroad_SupersededUpdate", PERSISTENT},
// PFEIFER - MAPD {{
{"MapdVersion", PERSISTENT},
{"RoadName", CLEAR_ON_ONROAD_TRANSITION},
{"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"MapAdvisorySpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION},
{"OSMDownloadBounds", PERSISTENT},
{"OSMDownloadLocations", PERSISTENT},
{"OsmDownloadedDate", PERSISTENT},
{"OsmStateTitle", PERSISTENT},
{"OsmStateName", PERSISTENT},
{"OSMDownloadProgress", CLEAR_ON_MANAGER_START},
{"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot
// }} PFEIFER - MAPD
};
} // namespace
-35
View File
@@ -1,35 +0,0 @@
# OSM Installation instructions:
# https://wiki.openstreetmap.org/wiki/Overpass_API/Installation
# Install expat. All other needed libraries are already installed.
# g++ make expat libexpat1-dev zlib1g-dev
#sudo apt-get update
#sudo apt-get install expat
# Add required path variables to environment
export OSM_ROOT=/data/media/0/osm
export OSM_VERSION=0.7.57
export GZ_FILE=osm-3s_v${OSM_VERSION}.tar.xz
export OSM_DIR=${OSM_ROOT}/v${OSM_VERSION}
# export DB_DIR=/data/osm/db/
# Download and extract overpass library
if [ ! -d ${OSM_ROOT} ]; then
mkdir -p ${OSM_ROOT}
fi
tar -vxf /data/openpilot/selfdrive/mapd/assets/${GZ_FILE} -C ${OSM_ROOT}
# Configure and install overpass
#cd $(ls | grep $SOURCE_FILE_ROOT\.[0-9]*)
#cd osm-3s_v0.7.56
#./configure CXXFLAGS="-O2" --prefix=$EXEC_DIR
#make install
# Remove source files after installation
#cd ..
if [ -d ${OSM_DIR} ]; then
rm -rf ${OSM_DIR}
fi
mv ${OSM_ROOT}/osm-3s_v${OSM_VERSION} ${OSM_ROOT}/v${OSM_VERSION}
-35
View File
@@ -1,35 +0,0 @@
export OSM_DIR=/data/media/0/osm
export DB_DIR=${OSM_DIR}/db
export OSM_LOCATION=$(cat /data/params/d/OsmLocationUrl)
export OSM_LOCATION_TEXT=$(cat /data/params/d/OsmLocationName)
export XZ_MAP_FILE_NAME=${OSM_LOCATION_TEXT}.tar.xz
export XZ_MAP_FILE=${OSM_DIR}/${XZ_MAP_FILE_NAME}
# WD
cd $OSM_DIR
# Remove legacy compressed map file if existing
rm -rf $XZ_MAP_FILE
# Download map file
wget -O ${XZ_MAP_FILE_NAME} ${OSM_LOCATION}
if [[ "$?" != 0 ]]; then
echo "Error downloading map file"
else
echo "Successfully downloaded map file"
# Remove current db dir if existing
rm -rf $DB_DIR
if [ -d ${OSM_DIR}/${OSM_LOCATION_TEXT} ]; then
rm -rf ${OSM_DIR}/${OSM_LOCATION_TEXT}
fi
# Decompressing
tar -vxf ${XZ_MAP_FILE_NAME}
mv ${OSM_LOCATION_TEXT} db
# Remove compressed map files after expanding
rm -rf $XZ_MAP_FILE
fi
+1 -6
View File
@@ -84,12 +84,7 @@ function launch {
# start manager
cd selfdrive/manager
if [ ! -f "/data/params/d/OsmLocal" ]; then
./custom_dep.py && ./build.py && ./manager.py
else
./custom_dep.py && ./build.py && ./local_osm_install.py && ./manager.py
fi
./build.py && ./mapd_installer.py && ./manager.py
# if broken, keep on screen error
while true; do sleep 1; done
}
+6 -10
View File
@@ -65,9 +65,6 @@ common/api/__init__.py
release/*
installer/custom/install_osm.sh
installer/custom/install_osm_db.sh
tools/__init__.py
tools/lib/*
tools/bodyteleop/*
@@ -155,11 +152,9 @@ selfdrive/debug/vw_mqb_config.py
selfdrive/gpxd/gpx_uploader.py
selfdrive/gpxd/gpxd.py
selfdrive/mapd/assets/*
selfdrive/mapd/config.py
selfdrive/mapd/mapd.py
selfdrive/mapd/lib/*
selfdrive/mapd/README.md
selfdrive/mapd_manager.py
selfdrive/sunnypilot/*.py
selfdrive/sunnypilot/live_map_data/*.py
common/SConscript
common/version.h
@@ -378,9 +373,8 @@ system/fleetmanager/*
selfdrive/manager/__init__.py
selfdrive/manager/build.py
selfdrive/manager/custom_dep.py
selfdrive/manager/helpers.py
selfdrive/manager/local_osm_install.py
selfdrive/manager/mapd_installer.py
selfdrive/manager/manager.py
selfdrive/manager/process_config.py
selfdrive/manager/process.py
@@ -480,6 +474,8 @@ third_party/acados/acados_template/**
third_party/bootstrap/**
third_party/qt5/larch64/bin/**
third_party/pfeiferj-mapd/**
scripts/update_now.sh
scripts/stop_updater.sh
@@ -52,5 +52,9 @@
"Offroad_Recalibration": {
"text": "openpilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield.",
"severity": 0
},
"Offroad_OSMUpdateRequired": {
"text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1",
"severity": 0
}
}
@@ -21,20 +21,20 @@ class SpeedLimitResolver:
Policy.combined: [Source.car_state, Source.nav, Source.map_data],
Policy.nav_only: [Source.nav]
}
self._reset_limit_sources()
for source in Source:
self._reset_limit_sources(source)
def change_policy(self, policy: Policy):
self._policy = policy
def _reset_limit_sources(self):
self._limit_solutions.clear()
self._distance_solutions.clear()
for source in Source:
self._limit_solutions[source] = 0.
self._distance_solutions[source] = 0.
def _reset_limit_sources(self, source):
self._limit_solutions[source] = 0.
self._distance_solutions[source] = 0.
def _is_sock_updated(self, sock):
return self._sm.alive[sock] and self._sm.updated[sock]
def resolve(self, v_ego, current_speed_limit, sm):
self._reset_limit_sources()
self._v_ego = v_ego
self._current_speed_limit = current_speed_limit
self._sm = sm
@@ -49,36 +49,44 @@ class SpeedLimitResolver:
self._get_from_map_data()
def _get_from_car_state(self):
if not self._is_sock_updated('carState'):
debug('SL: No carState instruction for speed limit')
return
self._reset_limit_sources(Source.car_state)
self._limit_solutions[Source.car_state] = self._sm['carState'].cruiseState.speedLimit
self._distance_solutions[Source.car_state] = 0.
def _get_from_nav(self):
if not self._sm.alive['navInstruction']:
if not self._is_sock_updated('navInstruction'):
debug('SL: No nav instruction for speed limit')
return
# Load limits from nav instruction
self._reset_limit_sources(Source.nav)
self._limit_solutions[Source.nav] = self._sm['navInstruction'].speedLimit
self._distance_solutions[Source.nav] = 0.
def _get_from_map_data(self):
sock = 'liveMapDataSP'
if self._sm.logMonoTime[sock] is None:
if not self._is_sock_updated(sock):
debug('SL: No map data for speed limit')
return
# Load limits from map_data
self._reset_limit_sources(Source.map_data)
self._process_map_data(self._sm[sock])
def _process_map_data(self, map_data):
speed_limit = map_data.speedLimit if map_data.speedLimitValid else 0.
next_speed_limit = map_data.speedLimitAhead if map_data.speedLimitAheadValid else 0.
gps_fix_age = time.time() - map_data.lastGpsTimestamp * 1e-3
if gps_fix_age > LIMIT_MAX_MAP_DATA_AGE:
debug(f'SL: Ignoring map data as is too old. Age: {gps_fix_age}')
return
speed_limit = map_data.speedLimit if map_data.speedLimitValid else 0.
next_speed_limit = map_data.speedLimitAhead if map_data.speedLimitAheadValid else 0.
self._calculate_map_data_limits(speed_limit, next_speed_limit, map_data)
def _calculate_map_data_limits(self, speed_limit, next_speed_limit, map_data):
-162
View File
@@ -1,162 +0,0 @@
#!/usr/bin/env python3
import os
import sys
import errno
import shutil
import tarfile
import time
import traceback
from common.basedir import BASEDIR
from common.text_window import TextWindow
from urllib.request import urlopen
from glob import glob
import subprocess
import importlib.util
from importlib.metadata import version
# NOTE: Do NOT import anything here that needs be built (e.g. params)
from common.spinner import Spinner
sys.path.append(os.path.join(BASEDIR, "third_party/mapd"))
OPSPLINE_SPEC = importlib.util.find_spec('scipy')
OVERPY_SPEC = importlib.util.find_spec('overpy')
MAX_BUILD_PROGRESS = 100
TMP_DIR = '/data/tmp'
THIRD_PARTY_DIR = '/data/openpilot/third_party/mapd'
THIRD_PARTY_DIR_SP = '/data/third_party_community'
PRELOADED_DEP_FILE = os.path.join(BASEDIR, "selfdrive/mapd/assets/mapd_deps.tar.xz")
OPSPLINE_VERSION = "1.11.1"
OVERPY_VERSION = "0.6"
SPECS = {
'scipy': OPSPLINE_VERSION,
'overpy': OVERPY_VERSION,
}
def wait_for_internet_connection(return_on_failure=False):
retries = 0
while True:
try:
_ = urlopen('https://www.google.com/', timeout=10)
return True
except Exception as e:
print(f'Wait for internet failed: {e}')
if return_on_failure and retries == 15:
return False
retries += 1
time.sleep(2) # Wait for 2 seconds before retrying
def install_dep(spinner):
wait_for_internet_connection()
TOTAL_PIP_STEPS = 2986
try:
os.makedirs(TMP_DIR)
except OSError as e:
if e.errno != errno.EEXIST:
raise
my_env = os.environ.copy()
my_env['TMPDIR'] = TMP_DIR
pip_target = [f'--target={THIRD_PARTY_DIR}']
packages = []
if OPSPLINE_SPEC is None:
packages.append(f'scipy=={OPSPLINE_VERSION}')
if OVERPY_SPEC is None:
packages.append(f'overpy=={OVERPY_VERSION}')
pip = subprocess.Popen([sys.executable, "-m", "pip", "install", "-v"] + pip_target + packages,
stdout=subprocess.PIPE, env=my_env)
# Read progress from pip and update spinner
steps = 0
while True:
output = pip.stdout.readline()
if pip.poll() is not None:
break
if output:
steps += 1
spinner.update_progress(MAX_BUILD_PROGRESS * min(1., steps / TOTAL_PIP_STEPS), 100.)
print(output.decode('utf8', 'replace'))
shutil.rmtree(TMP_DIR)
os.unsetenv('TMPDIR')
# remove numpy installed to THIRD_PARTY_DIR since numpy is already present in the AGNOS image
if OPSPLINE_SPEC is None:
for directory in glob(f'{THIRD_PARTY_DIR}/numpy*'):
shutil.rmtree(directory)
if os.path.exists(f'{THIRD_PARTY_DIR}/bin'):
shutil.rmtree(f'{THIRD_PARTY_DIR}/bin')
dup = f'cp -rf {THIRD_PARTY_DIR} {THIRD_PARTY_DIR_SP}'
process_dup = subprocess.Popen(dup, stdout=subprocess.PIPE, shell=True)
if __name__ == "__main__":
reload_required = False
for package, req_version in SPECS.items():
package_spec = importlib.util.find_spec(package)
if package_spec is not None and version(package) != req_version:
print(f"SP_LOG: current {package} is {version(package)}, requires {req_version}. Removing directory {THIRD_PARTY_DIR}...")
reload_required = True
if reload_required:
command = f'rm -rf {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
if OPSPLINE_SPEC is None or OVERPY_SPEC is None or reload_required:
spinner = Spinner()
preload_fault = False
try:
if os.path.exists(PRELOADED_DEP_FILE):
spinner.update("Loading preloaded dependencies")
try:
with tarfile.open(PRELOADED_DEP_FILE, "r:xz") as tar:
for member in tar.getmembers():
split_components = member.name.split('/')
if len(split_components) > 1:
member.name = '/'.join(split_components[1:])
tar.extract(member, path=THIRD_PARTY_DIR)
print(f"SP_LOG: Preloaded dependencies extracted to {THIRD_PARTY_DIR}")
except Exception as e:
preload_fault = True
command = f'rm -rf {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
print(f"SP_LOG: An error occurred while extracting preloaded dependencies: {e}")
print(f"SP_LOG: Cleanup directory {e}")
if not os.path.exists(PRELOADED_DEP_FILE) or preload_fault:
if os.path.exists(THIRD_PARTY_DIR_SP):
try:
spinner.update("Loading cached dependencies")
command = f'rm -rf {THIRD_PARTY_DIR}; cp -rf {THIRD_PARTY_DIR_SP} {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
print(f"SP_LOG: Removed directory {THIRD_PARTY_DIR}")
print(f"SP_LOG: Copied {THIRD_PARTY_DIR_SP} to {THIRD_PARTY_DIR}")
except Exception as e:
command = f'rm -rf {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
print(f"SP_LOG: An error occurred while loading cached dependencies: {e}")
print(f"SP_LOG: Cleanup directory {e}")
else:
spinner.update("Waiting for internet")
try:
install_dep(spinner)
except Exception as e:
command = f'rm -rf {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
print(f"SP_LOG: An error occurred while downloading dependencies: {e}")
print(f"SP_LOG: Cleanup directory {e}")
except Exception:
command = f'rm -rf {THIRD_PARTY_DIR}'
process = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
import selfdrive.sentry as sentry
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
error = traceback.format_exc(-3)
error = "Dependency Manager failed to start\n\n" + error
with TextWindow(error) as t:
t.wait_for_exit()
-72
View File
@@ -1,72 +0,0 @@
#!/usr/bin/env python3
import os
import subprocess
import sys
import time
import traceback
# NOTE: Do NOT import anything here that needs be built (e.g. params)
from common.basedir import BASEDIR
from common.params import Params
from common.spinner import Spinner
from common.text_window import TextWindow
import selfdrive.sentry as sentry
from selfdrive.manager.custom_dep import wait_for_internet_connection
def install_local_osm(_spinner):
_install(_spinner, "./install_osm.sh", "Installing OSM Server")
def install_osm_db(_spinner):
_install(_spinner, "./install_osm_db.sh", "Installing OSM DB - " + Params().get("OsmLocationName", encoding="utf-8"))
def _install(_spinner, script, title):
_spinner.update(title)
process = subprocess.Popen(['sh', script], cwd=os.path.join(BASEDIR, 'installer/custom/'),
stdout=subprocess.PIPE)
# Read progress from install script and update spinner
frame = 0
while True:
output = process.stdout.readline()
if process.poll() is not None:
break
_spinner.update(title + (".".replace(".", "." * (frame % 5), 1)))
frame += 1
print(output.decode('utf8', 'replace'))
time.sleep(0.1)
if __name__ == "__main__":
try:
from selfdrive.mapd.lib.helpers import is_local_osm_installed, timestamp_local_osm_db, is_osm_db_up_to_date, OSM_LOCAL_PATH
sys.path.append(os.path.join(BASEDIR, "third_party/mapd"))
params = Params()
update_osm_db_check = params.get_bool("OsmDbUpdatesCheck")
if not (os.path.exists(f"{OSM_LOCAL_PATH}/db") or
os.path.exists(f"{OSM_LOCAL_PATH}/v0.7.57")) or update_osm_db_check:
spinner = Spinner()
spinner.update("Waiting for internet connection...")
if wait_for_internet_connection(return_on_failure=True):
is_osm_installed = is_local_osm_installed(params)
is_db_updated = is_osm_db_up_to_date()
print(f'Local OSM Installer:\nOSM currently installed: {is_osm_installed}\nDB up to date: {is_db_updated}')
if not is_osm_installed:
install_local_osm(spinner)
if not is_db_updated:
install_osm_db(spinner)
timestamp_local_osm_db()
spinner.close()
params.put_bool("OsmDbUpdatesCheck", False)
except Exception:
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
error = traceback.format_exc(-3)
error = "OSM Offline Database Manager failed to start\n\n" + error
with TextWindow(error) as t:
t.wait_for_exit()
+5
View File
@@ -16,6 +16,7 @@ from openpilot.common.text_window import TextWindow
from openpilot.selfdrive.boardd.set_time import set_time
from openpilot.system.hardware import HARDWARE, PC
from openpilot.selfdrive.manager.helpers import unblock_stdout, write_onroad_params
from openpilot.selfdrive.manager.mapd_installer import VERSION
from openpilot.selfdrive.manager.process import ensure_running
from openpilot.selfdrive.manager.process_config import managed_processes
from openpilot.selfdrive.athena.registration import register, UNREGISTERED_DONGLE_ID, is_registered_device
@@ -100,6 +101,10 @@ def manager_init() -> None:
("TurnVisionControl", "0"),
("VisionCurveLaneless", "0"),
("VwAccType", "0"),
("OsmDbUpdatesCheck", "0"),
("OsmDownloadedDate", "0"),
("OSMDownloadProgress", "{}"),
("MapdVersion", f"{VERSION}"),
]
if not PC:
default_params.append(("LastUpdateTime", datetime.datetime.utcnow().isoformat().encode('utf8')))
+143
View File
@@ -0,0 +1,143 @@
#!/usr/bin/env python3
import logging
import os
import stat
import time
import traceback
import requests
from pathlib import Path
from urllib.request import urlopen
import openpilot.selfdrive.sentry as sentry
from cereal import messaging
from common.spinner import Spinner
from common.params import Params
from openpilot.selfdrive.mapd_manager import COMMON_DIR, MAPD_PATH, MAPD_BIN_DIR
from openpilot.system.version import is_prebuilt
VERSION = 'v1.8.0'
URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd"
class MapdInstallManager:
def __init__(self, spinner_ref: Spinner):
self._spinner = spinner_ref
def download(self):
self.ensure_directories_exist()
self._download_file()
self.update_installed_version(VERSION)
def check_and_download(self):
if self.download_needed():
self.download()
@staticmethod
def download_needed():
return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION
@staticmethod
def ensure_directories_exist():
if not os.path.exists(COMMON_DIR):
os.makedirs(COMMON_DIR)
if not os.path.exists(MAPD_BIN_DIR):
os.makedirs(MAPD_BIN_DIR)
@staticmethod
def _safe_write_and_set_executable(file_path, content):
with open(file_path, 'wb') as output:
output.write(content)
output.flush()
os.fsync(output.fileno())
current_permissions = stat.S_IMODE(os.lstat(file_path).st_mode)
os.chmod(file_path, current_permissions | stat.S_IEXEC)
def _download_file(self, num_retries=5):
temp_file = Path(MAPD_PATH + ".tmp")
download_timeout = 60
for cnt in range(num_retries):
try:
response = requests.get(URL, stream=True, timeout=download_timeout)
response.raise_for_status()
self._safe_write_and_set_executable(temp_file, response.content)
# No exceptions encountered. Safe to replace original file.
temp_file.replace(MAPD_PATH)
return
except requests.exceptions.ReadTimeout:
self._spinner.update(f"ReadTimeout caught. Timeout is [{download_timeout}]. Retrying download... [{cnt}]")
time.sleep(0.5)
except requests.exceptions.RequestException as e:
self._spinner.update(f"RequestException caught: {e}. Retrying download... [{cnt}]")
time.sleep(0.5)
# Delete temp file if the process was not successful.
if temp_file.exists():
temp_file.unlink()
logging.error("Failed to download file after all retries")
@staticmethod
def update_installed_version(version):
Params().put("MapdVersion", version)
@staticmethod
def get_installed_version():
return Params().get("MapdVersion", encoding="utf-8")
def wait_for_internet_connection(self, return_on_failure=False):
max_retries = 10
for retries in range(max_retries+1):
self._spinner.update(f"Waiting for internet connection... [{retries}/{max_retries}]")
time.sleep(2)
try:
_ = urlopen('https://sentry.io', timeout=10)
return True
except Exception as e:
print(f'Wait for internet failed: {e}')
if return_on_failure and retries == max_retries:
return False
def non_prebuilt_install(self):
sm = messaging.SubMaster(['deviceState'])
metered = sm['deviceState'].networkMetered
if metered:
self._spinner.update(f"Can't proceed with mapd install since network is metered!")
time.sleep(5)
return False
try:
self.ensure_directories_exist()
if not self.download_needed():
self._spinner.update("Mapd is good!")
time.sleep(0.1)
return True
if self.wait_for_internet_connection(return_on_failure=True):
self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].")
time.sleep(0.1)
self.check_and_download()
self._spinner.close()
except Exception:
for i in range(6):
self._spinner.update(f"Failed to download OSM maps won't work until properly downloaded!"
f"Try again manually rebooting. "
f"Boot will continue in {5 - i}s...")
time.sleep(1)
sentry.init(sentry.SentryProject.SELFDRIVE)
traceback.print_exc()
sentry.capture_exception()
if __name__ == "__main__":
spinner = Spinner()
install_manager = MapdInstallManager(spinner)
install_manager.ensure_directories_exist()
if is_prebuilt():
debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]"
spinner.update(debug_msg)
install_manager.update_installed_version(VERSION)
else:
spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]")
time.sleep(1)
install_manager.non_prebuilt_install()
+6 -1
View File
@@ -4,6 +4,7 @@ from cereal import car
from openpilot.common.params import Params
from openpilot.system.hardware import PC, TICI
from openpilot.selfdrive.manager.process import PythonProcess, NativeProcess, DaemonProcess
from openpilot.selfdrive.mapd_manager import MAPD_PATH, COMMON_DIR
WEBCAM = os.getenv("USE_WEBCAM") is not None
@@ -82,7 +83,11 @@ procs = [
PythonProcess("statsd", "selfdrive.statsd", always_run),
NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None), always_watchdog=True),
PythonProcess("mapd", "selfdrive.mapd.mapd", only_onroad),
# PFEIFER - MAPD {{
NativeProcess("mapd", COMMON_DIR, [MAPD_PATH], always_run),
PythonProcess("mapd_manager", "selfdrive.mapd_manager", always_run),
# }} PFEIFER - MAPD
PythonProcess("otisserv", "selfdrive.navd.otisserv", always_run),
PythonProcess("fleet_manager", "system.fleetmanager.fleet_manager", always_run),
-8
View File
@@ -1,8 +0,0 @@
# MapD
The OpenStreetMap-based speed logical by the [Move Fast team](https://github.com/move-fast), [dragonpilot team](https://github.com/dragonpilot-community/dragonpilot), and additional improvements by [sunnypilot](https://github.com/sunnyhaibin/sunnypilot).
The comma three uses regular SciPy. To have a better experience with `mapd`, please go to [OpenStreetMap](https://openstreetmap.org) to update and improve your area's data (i.e., Speed Limit, Stop Signs, Traffic Lights).
To use `mapd`, you consent to `mapd` uploading the traces. You may opt out of uploading traces at any time.
© OpenStreetMap contributors
Binary file not shown.
Binary file not shown.
-8
View File
@@ -1,8 +0,0 @@
# Map query config
QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries.
QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries.
MIN_DISTANCE_FOR_NEW_QUERY = 1000 # mts. Minimum distance to query area edge before issuing a new query.
FULL_STOP_MAX_SPEED = 1.39 # m/s Max speed for considering car is stopped.
LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg.
LANE_WIDTH = 3.7 # Lane width estimate. Used for detecting departures from way.
-452
View File
@@ -1,452 +0,0 @@
import numpy as np
from enum import Enum
from selfdrive.mapd.lib.geo import DIRECTION, R, vectors
from scipy.interpolate import splev, splprep
_TURN_CURVATURE_THRESHOLD = 0.002 # 1/mts. A curvature over this value will generate a speed limit section.
_MAX_LAT_ACC = 2. # Maximum lateral acceleration in turns.
_SPLINE_EVAL_STEP = 5 # mts for spline evaluation for curvature calculation
_MIN_SPEED_SECTION_LENGTH = 100. # mts. Sections below this value will not be split in smaller sections.
_MAX_CURV_DEVIATION_FOR_SPLIT = 2. # Split a speed section if the max curvature deviates from mean by this factor.
_MAX_CURV_SPLIT_ARC_ANGLE = 90. # degrees. Arc section to split into new speed section around max curvature.
_MIN_NODE_DISTANCE = 50. # mts. Minimum distance between nodes for spline evaluation. Data is enhanced if not met.
_ADDED_NODES_DIST = 15. # mts. Distance between added nodes when data is enhanced for spline evaluation.
_DIVERTION_SEARCH_RANGE = [-200., 50.] # mt. Range of distance to current location for diversion search.
def nodes_raw_data_array_for_wr(wr, drop_last=False, feature_sl=False):
"""Provides an array of raw node data (id, lat, lon, speed_limit, advisory_speed_limit) for all nodes in way relation
"""
sl = wr.speed_limit
asl = wr.advisory_speed_limit
data = np.array([(n.id, n.lat, n.lon, sl, asl) for n in wr.way.nodes], dtype=float)
if feature_sl:
for count, node in enumerate(wr.way.nodes):
if 'highway' in node.tags:
if node.tags['highway'] == 'mini_roundabout':
data[count][3] = 4.1667
if 'direction' in node.tags and (node.tags['highway'] == 'stop' or node.tags['highway'] == 'give_way'):
if (wr.direction == DIRECTION.BACKWARD and node.tags['direction'] == 'backward') or (wr.direction == DIRECTION.FORWARD and node.tags['direction'] == 'forward'):
if node.tags['highway'] == 'give_way':
data[count][3] = 2.7777
if node.tags['highway'] == 'stop':
data[count][3] = 0.1
if 'traffic_calming' in node.tags:
if node.tags['traffic_calming'] == 'yes':
data[count][3] = 40/3.6
if node.tags['traffic_calming'] == 'chicane' or node.tags['traffic_calming'] == 'choker':
data[count][3] = 20/3.6
if node.tags['traffic_calming'] == 'bump':
data[count][3] = 2.24
if node.tags['traffic_calming'] == 'hump':
data[count][3] = 8.94
# reverse the order if way direction is backwards
if wr.direction == DIRECTION.BACKWARD:
data = np.flip(data, axis=0)
# drop last if requested
return data[:-1] if drop_last else data
def node_calculations(points):
"""Provides node calculations based on an array of (lat, lon) points in radians.
points is a (N x 1) array where N >= 3
"""
if len(points) < 3:
raise(IndexError)
# Get the vector representation of node points in cartesian plane.
# (N-1, 2) array. Not including (0., 0.)
v = vectors(points) * R
# Calculate the vector magnitudes (or distance)
# (N-1, 1) array. No distance for v[-1]
d = np.linalg.norm(v, axis=1)
# Calculate the bearing (from true north clockwise) for every node.
# (N-1, 1) array. No bearing for v[-1]
b = np.arctan2(v[:, 0], v[:, 1])
# Add origin to vector space. (i.e first node in list)
v = np.concatenate(([[0., 0.]], v))
# Provide distance to previous node and distance to next node
dp = np.concatenate(([0.], d))
dn = np.concatenate((d, [0.]))
# Provide cumulative distance on route
dr = np.cumsum(dp, axis=0)
# Bearing of last node should keep bearing from previous.
b = np.concatenate((b, [b[-1]]))
return v, dp, dn, dr, b
def spline_curvature_calculations(vect, dist_prev):
"""Provides an array of curvatures and its distances by applying a spline interpolation
to the path described by the nodes data.
"""
# We need to artificially enhance the data before applying spline interpolation to avoid getting
# inexistent curvature values close to irregularities on the road when the resolution of nodes data
# approaching the irregularity is low.
# - Find indexes where dist_prev is greater than threshold
too_far_idxs = np.nonzero(dist_prev >= _MIN_NODE_DISTANCE)[0]
# - Traversing in reverse order, enhance data by adding points at the found indexes.
for idx in too_far_idxs[::-1]:
dp = dist_prev[idx] # distance of vector that needs to be replaced by higher resolution vectors.
n = int(np.ceil(dp / _ADDED_NODES_DIST)) # number of vectors that need to be added.
new_v = vect[idx, :] / n # new relative vector to insert.
vect = np.delete(vect, idx, axis=0) # remove the relative vector to be replaced by the insertion of new vectors.
vect = np.insert(vect, [idx] * n, [new_v] * n, axis=0) # insert n new relative vectors
# Data is now enhanced, we can proceed with curvature evaluation.
# - Create cumulative arrays for distance traveled and vector (x, y)
ds = np.cumsum(dist_prev, axis=0)
vs = np.cumsum(vect, axis=0)
# - spline interpolation
tck, u = splprep([vs[:, 0], vs[:, 1]]) # pylint: disable=unbalanced-tuple-unpacking
# - evaluate every _SPLINE_EVAL_STEP mts.
n = max(int(ds[-1] / _SPLINE_EVAL_STEP), len(u))
unew = np.arange(0, n + 1) / n
# - get derivatives
d1 = splev(unew, tck, der=1)
d2 = splev(unew, tck, der=2)
# - calculate curvatures
num = d1[0] * d2[1] - d1[1] * d2[0]
den = (d1[0]**2 + d1[1]**2)**(1.5)
curv = num / den
curv_ds = unew * ds[-1]
return curv, curv_ds
def speed_section(curv_sec):
"""Map curvature section data into turn speed sections data.
Returns: [section start distance, section end distance, speed limit based on max curvature, sing of curvature]
"""
max_curv_idx = np.argmax(curv_sec[:, 0])
start = np.amin(curv_sec[:, 2])
end = np.amax(curv_sec[:, 2])
return np.array([start, end, np.sqrt(_MAX_LAT_ACC / curv_sec[max_curv_idx, 0]), curv_sec[max_curv_idx, 1]])
def split_speed_section_by_sign(curv_sec):
"""Will split the given curvature section in subsections if there is a change of sign on the curvature value
in the section.
"""
# Find the indexes where the curvatures change signs (if any).
c_idx = np.nonzero(np.diff(curv_sec[:, 1]))[0] + 1
# Split section base on change of sign.
return np.split(curv_sec, c_idx)
def split_speed_section_by_curv_degree(curv_sec):
"""Will split the given curvature section in subsections as to isolate peaks of turn with substantially
higher curvature values. This will aid on preventing having very long turn sections with low speed limit
that is only really necessary for a small region of the section.
"""
# Only consider splitting a section if long enough.
length = curv_sec[-1, 2] - curv_sec[0, 2]
if length <= _MIN_SPEED_SECTION_LENGTH:
return [curv_sec]
# Only split if max curvature deviates substantially from mean curvature.
max_curv_idx = np.argmax(curv_sec[:, 0])
max_curv = curv_sec[max_curv_idx, 0]
mean_curv = np.mean(curv_sec[:, 0])
if max_curv / mean_curv <= _MAX_CURV_DEVIATION_FOR_SPLIT:
return [curv_sec]
# Calculate where to split as to isolate a curve section around the max curvature peak.
arc_side = (np.radians(_MAX_CURV_SPLIT_ARC_ANGLE) / max_curv) / 2.
arc_side_idx_lenght = int(np.ceil(arc_side / _SPLINE_EVAL_STEP))
split_idxs = [max_curv_idx - arc_side_idx_lenght, max_curv_idx + arc_side_idx_lenght]
split_idxs = list(filter(lambda idx: idx > 0 and idx < len(curv_sec) - 1, split_idxs))
# If the arc section to split extendes outside the section, then no need to split.
if len(split_idxs) == 0:
return [curv_sec]
# Create the splits and split the resulting sections recursevly.
splits = [split_speed_section_by_curv_degree(cs) for cs in np.split(curv_sec, split_idxs)]
# Flatten the results and return the new list of curvature sections.
curv_secs = [cs for split in splits for cs in split]
return curv_secs
def speed_limits_for_curvatures_data(curv, dist):
"""Provides the calculations for the speed limits from the curvatures array and distances,
by providing distances to curvature sections and corresponding speed limit values as well as
curvature direction/sign.
"""
# Prepare a data array for processing with absolute curvature values, curvature sign and distances.
curv_abs = np.abs(curv)
data = np.column_stack((curv_abs, np.sign(curv), dist))
# Find where curvatures overshoot turn curvature threshold and define as section
is_section = curv_abs >= _TURN_CURVATURE_THRESHOLD
# Find the indexes where the sections start and end. i.e. change indexes.
c_idx = np.nonzero(np.diff(is_section))[0] + 1
# Create independent arrays for each split section base on change indexes.
splits = np.array(np.split(data, c_idx), dtype=object)
# Filter the splits to keep only the curvature section arrays by getting the odd or even split arrays depending
# on whether the first split is a curvature split or not.
curv_sec_idxs = np.arange(0 if is_section[0] else 1, len(splits), 2, dtype=int)
curv_secs = splits[curv_sec_idxs]
# Further split the curv sections by sign change
sub_secs = [split_speed_section_by_sign(cs) for cs in curv_secs]
curv_secs = [cs for sub_sec in sub_secs for cs in sub_sec]
# Further split the curv sections by degree of curvature
sub_secs = [split_speed_section_by_curv_degree(cs) for cs in curv_secs]
curv_secs = [cs for sub_sec in sub_secs for cs in sub_sec]
# Return an array where each row represents a turn speed limit section.
# [start, end, speed_limit, curvature_sign]
return np.array([speed_section(cs) for cs in curv_secs])
def is_wr_a_valid_divertion_from_node(wr, node_id, wr_ids):
"""
Evaluates if the way relation `wr` is a valid diversion from node with id `node_id`.
A valid diversion is a way relation with an edge node with the given `node_id` that is not already included
in the list of way relations in the route (`wr_ids`) and that can be travaled in the direction as if starting
from node with id `node_id`
"""
if wr.id in wr_ids:
return False
wr.update_direction_from_starting_node(node_id)
return not wr.is_prohibited
class SpeedLimitSection():
"""And object representing a speed limited road section ahead.
provides the start and end distance and the speed limit value
"""
def __init__(self, start, end, value):
self.start = start
self.end = end
self.value = value
def __repr__(self):
return f'from: {self.start}, to: {self.end}, limit: {self.value}'
class TurnSpeedLimitSection(SpeedLimitSection):
def __init__(self, start, end, value, sign):
super().__init__(start, end, value)
self.curv_sign = sign
def __repr__(self):
return f'{super().__repr__()}, sign: {self.curv_sign}'
class NodeDataIdx(Enum):
"""Column index for data elements on NodesData underlying data store.
"""
node_id = 0
lat = 1
lon = 2
speed_limit = 3
advisory_speed_limit = 4
x = 5 # x value of cartesian vector representing the section between last node and this node.
y = 6 # y value of cartesian vector representing the section between last node and this node.
dist_prev = 7 # distance to previous node.
dist_next = 8 # distance to next node
dist_route = 9 # cumulative distance on route
bearing = 10 # bearing of the vector departing from this node.
class NodesData:
"""Container for the list of node data from a ordered list of way relations to be used in a Route
"""
def __init__(self, way_relations, wr_index):
self._nodes_data = np.array([])
self._divertions = [[]]
self._curvature_speed_sections_data = np.array([])
way_count = len(way_relations)
if way_count == 0:
return
# We want all the nodes from the last way section
nodes_data = nodes_raw_data_array_for_wr(way_relations[-1])
# For the ways before the last in the route we want all the nodes but the last, as that one is the first on
# the next section. Collect them, append last way node data and concatenate the numpy arrays.
if way_count > 1:
wrs_data = tuple([nodes_raw_data_array_for_wr(wr, drop_last=True) for wr in way_relations[:-1]])
wrs_data += (nodes_data,)
nodes_data = np.concatenate(wrs_data)
# Get a subarray with lat, lon to compute the remaining node values.
lat_lon_array = nodes_data[:, [1, 2]]
points = np.radians(lat_lon_array)
# Ensure we have more than 3 points, if not calculations are not possible.
if len(points) <= 3:
return
vect, dist_prev, dist_next, dist_route, bearing = node_calculations(points)
# append calculations to nodes_data
# nodes_data structure: [id, lat, lon, speed_limit, advisory_speed_limit, x, y, dist_prev, dist_next, dist_route, bearing]
self._nodes_data = np.column_stack((nodes_data, vect, dist_prev, dist_next, dist_route, bearing))
# Build route diversion options data from the wr_index.
wr_ids = [wr.id for wr in way_relations]
self._divertions = [[wr for wr in wr_index.way_relations_with_edge_node_id(node_id)
if is_wr_a_valid_divertion_from_node(wr, node_id, wr_ids)]
for node_id in nodes_data[:, 0]]
# Store calculcations for curvature sections speed limits. We need more than 3 points to be able to process.
# _curvature_speed_sections_data structure: [dist_start, dist_stop, speed_limits, curv_sign]
if len(vect) > 3:
curv, curv_ds = spline_curvature_calculations(vect, dist_prev)
self._curvature_speed_sections_data = speed_limits_for_curvatures_data(curv, curv_ds)
@property
def count(self):
return len(self._nodes_data)
def get(self, node_data_idx):
"""Returns the array containing all the elements of a specific NodeDataIdx type.
"""
if len(self._nodes_data) == 0 or node_data_idx.value >= self._nodes_data.shape[1]:
return np.array([])
return self._nodes_data[:, node_data_idx.value]
def speed_limits_ahead(self, ahead_idx, distance_to_node_ahead):
"""Returns and array of SpeedLimitSection objects for the actual route ahead of current location
"""
if len(self._nodes_data) == 0 or ahead_idx is None:
return []
# Find the cumulative distances where speed limit changes. Build Speed limit sections for those.
dist = np.concatenate(([distance_to_node_ahead], self.get(NodeDataIdx.dist_next)[ahead_idx:]))
dist = np.cumsum(dist, axis=0)
sl = self.get(NodeDataIdx.speed_limit)[ahead_idx - 1:]
sl_next = np.concatenate((sl[1:], [0.]))
# Create a boolean mask where speed limit changes and filter values
sl_change = sl != sl_next
distances = dist[sl_change]
speed_limits = sl[sl_change]
# Create speed limits sections combining all continuous nodes that have same speed limit value.
start = 0.
limits_ahead = []
for idx, end in enumerate(distances):
limits_ahead.append(SpeedLimitSection(start, end, speed_limits[idx]))
start = end
return limits_ahead
def advisory_speed_limits_ahead(self, ahead_idx, distance_to_node_ahead):
"""Returns and array of SpeedLimitSection objects for the actual route ahead of current location
"""
if len(self._nodes_data) == 0 or ahead_idx is None:
return []
# Find the cumulative distances where speed limit changes. Build Speed limit sections for those.
dist = np.concatenate(([distance_to_node_ahead], self.get(NodeDataIdx.dist_next)[ahead_idx:]))
dist = np.cumsum(dist, axis=0)
sl = self.get(NodeDataIdx.advisory_speed_limit)[ahead_idx - 1:]
sl_next = np.concatenate((sl[1:], [0.]))
# Create a boolean mask where speed limit changes and filter values
sl_change = sl != sl_next
distances = dist[sl_change]
speed_limits = sl[sl_change]
# Create speed limits sections combining all continuous nodes that have same speed limit value.
start = 0.
limits_ahead = []
for idx, end in enumerate(distances):
if speed_limits[idx] != None and speed_limits[idx] > 0:
limits_ahead.append(SpeedLimitSection(start, end, speed_limits[idx]))
start = end
return limits_ahead
def distance_to_end(self, ahead_idx, distance_to_node_ahead):
if len(self._nodes_data) == 0 or ahead_idx is None:
return None
return np.sum(np.concatenate(([distance_to_node_ahead], self.get(NodeDataIdx.dist_next)[ahead_idx:])))
def curvatures_speed_limit_sections_ahead(self, ahead_idx, distance_to_node_ahead):
"""Returns and array of TurnSpeedLimitSection objects for the actual route ahead of current location for
speed limit sections due to curvatures in the road.
"""
if len(self._curvature_speed_sections_data) == 0 or ahead_idx is None:
return []
# Find the current distance traveled so far on the route.
dist_curr = self.get(NodeDataIdx.dist_route)[ahead_idx] - distance_to_node_ahead
# Filter the sections to get only those where the stop distance is ahead of current.
sec_filter = self._curvature_speed_sections_data[:, 1] > dist_curr
data = self._curvature_speed_sections_data[sec_filter]
# Offset distances to current distance.
data[:, [0, 1]] -= dist_curr
# Create speed limits sections
limits_ahead = [TurnSpeedLimitSection(max(0., d[0]), d[1], d[2], d[3]) for d in data]
advisory_speed_limits_ahead = self.advisory_speed_limits_ahead(ahead_idx, distance_to_node_ahead)
for advisory_limit in advisory_speed_limits_ahead:
for limit in limits_ahead:
if limit.start >= advisory_limit.start and limit.end <= advisory_limit.end:
limit.value = advisory_limit.value
return limits_ahead
def possible_divertions(self, ahead_idx, distance_to_node_ahead):
""" Returns and array with the way relations the route could possible divert to by finding
the alternative way diversions on the nodes in the vicinity of the current location.
"""
if len(self._nodes_data) == 0 or ahead_idx is None:
return []
dist_route = self.get(NodeDataIdx.dist_route)
rel_dist = dist_route - dist_route[ahead_idx] + distance_to_node_ahead
valid_idxs = np.nonzero(np.logical_and(rel_dist >= _DIVERTION_SEARCH_RANGE[0],
rel_dist <= _DIVERTION_SEARCH_RANGE[1]))[0]
valid_divertions = [self._divertions[i] for i in valid_idxs]
return [wr for wrs in valid_divertions for wr in wrs] # flatten.
def distance_to_node(self, node_id, ahead_idx, distance_to_node_ahead):
"""
Provides the distance to a specific node in the route identified by `node_id` in reference to the node ahead
(`ahead_idx`) and the distance from current location to the node ahead (`distance_to_node_ahead`).
"""
node_ids = self.get(NodeDataIdx.node_id)
node_idxs = np.nonzero(node_ids == node_id)[0]
if len(self._nodes_data) == 0 or ahead_idx is None or len(node_idxs) == 0:
return None
return self.get(NodeDataIdx.dist_route)[node_idxs[0]] - self.get(NodeDataIdx.dist_route)[ahead_idx] + \
distance_to_node_ahead
-343
View File
@@ -1,343 +0,0 @@
from selfdrive.mapd.lib.NodesData import NodesData, NodeDataIdx
from selfdrive.mapd.config import QUERY_RADIUS
from selfdrive.mapd.lib.geo import ref_vectors, R, distance_to_points
from itertools import compress
import numpy as np
_ACCEPTABLE_BEARING_DELTA_COSINE = -0.7 # Continuation paths with a bearing of 180 +/- 45 degrees.
_MAX_ALLOWED_BEARING_DELTA_COSINE_AT_EDGE = -0.3420 # bearing delta at route edge must be 180 +/- 70 degrees.
_MAP_DATA_EDGE_DISTANCE = 50 # mts. Consider edge of map data from this distance to edge of query radius.
class Route():
"""A set of consecutive way relations forming a default driving route.
"""
def __init__(self, current, wr_index, way_collection_id, query_center):
"""Create a Route object from a given `wr_index` (Way relation index)
Args:
current (WayRelation): The Way Relation that is currently located. It must be active.
wr_index (WayRelationIndex): The indexes of WayRelations by node id.
way_collection_id (UUID): The id of the Way Collection that created this Route.
query_center (Numpy Array): lat, lon] numpy array in radians indicating the center of the data query.
"""
self.way_collection_id = way_collection_id
self._ordered_way_relations = []
self._nodes_data = None
self._reset()
# An active current way is needed to be able to build a route
if not current.active:
return
# Build the route by finding iteratavely the best matching ways continuing after the end of the
# current (last_wr) way. Use the index to find the continuation possibilities on each iteration.
last_wr = current
ordered_way_ids = []
split_wrs = []
while True:
try:
# - Append current element to the route list of ordered way relations.
self._ordered_way_relations.append(last_wr)
ordered_way_ids.append(last_wr.id)
# - Get the id of the node at the end of the way and then fetch the way relations that share the end node id.
last_node_id = last_wr.last_node.id
way_relations = wr_index.way_relations_with_edge_node_id(last_node_id)
# - Add split way relations when necessary and remove parent way relations.
split_wrs_to_add = [wr for wr in split_wrs if last_node_id in wr.edge_nodes_ids]
way_relations.extend(split_wrs_to_add)
parent_ids = [wr.parent_wr_id for wr in split_wrs_to_add]
way_relations = [wr for wr in way_relations if wr.id not in parent_ids]
# - If no more way_relations than last_wr, we have to check if we join another wr on an internal node, and
# if we do, we replace such way relation with the split of it and continue.
if len(way_relations) == 1:
way_relations = wr_index.way_relations_with_node_id(last_node_id)
# If no more way_relations than last_wr or its parent, we got to the end.
if len(way_relations) == 1:
break
# If last_wr is a split, replace its parent with last_wr
way_relations = [last_wr if wr is last_wr.parent else wr for wr in way_relations]
# If we join a wr on an internal node, then we artificially split the wr in two and pass both wrs as
# candidates to the wr selection code below.
wr_to_split = [wr for wr in way_relations if wr is not last_wr][0]
next_split_way_id = -len(split_wrs) - 1 # Keep split wrs ids unique on Route
new_wrs = wr_to_split.split(last_node_id, [next_split_way_id, next_split_way_id - 1])
# If it could not be splited, we are done.
if len(new_wrs) != 2:
break
# Replace the original way relation for the splitted version on way_relations and track splited wrs.
split_wrs.extend(new_wrs)
way_relations.remove(wr_to_split)
way_relations.extend(new_wrs)
# - Get the coordinates for the edge node and build the array of coordinates for the nodes before the edge node
# on each of the common way relations, then get the vectors in cartesian plane for the end sections of each way.
ref_point = last_wr.last_node_coordinates
points = np.array([wr.node_before_edge_coordinates(last_node_id) for wr in way_relations])
v = ref_vectors(ref_point, points) * R
# - Calculate the bearing (from true north clockwise) for every end section of each way.
b = np.arctan2(v[:, 0], v[:, 1])
# - Find index of las_wr section and calculate deltas of bearings to the other sections.
last_wr_idx = way_relations.index(last_wr)
b_ref = b[last_wr_idx]
delta = b - b_ref
# - Update the direction of the possible route continuation ways as starting from last_node_id.
# Make sure to exclude any ways already included in the ordered list as to not modify direction when there
# are looping roads (like roundabouts). A way will never be included twice in a route anyway.
for wr in way_relations:
if wr.id not in ordered_way_ids:
wr.update_direction_from_starting_node(last_node_id)
# - Filter the possible route continuation way relations:
# - exclude any way already added to the ordered list.
# - exclude all way relations that are prohibited due to traffic direction.
mask = [wr.id not in ordered_way_ids and not wr.is_prohibited for wr in way_relations]
way_relations = list(compress(way_relations, mask))
delta = delta[mask]
# if no options left, we got to the end.
if len(way_relations) == 0:
break
# - The cosine of the bearing delta will aid us in choosing the way that continues. The cosine is
# minimum (-1) for a perfect straight continuation as delta would be pi or -pi.
cos_delta = np.cos(delta)
def pick_best_idx(cos_delta):
"""Selects the best index on `cos_delta` array for a way that continues the route.
In principle we want to choose the way that continues as straight as possible.
Bue we need to make sure that if there are 2 or more ways continuing relatively straight, then we
need to disambiguate, either by matching the `ref` or `name` value of the continuing way with the
last way selected.
This can prevent cases where the chosen route could be for instance an exit ramp of a way due to the fact
that the ramp has a better match on bearing to previous way. We choose to stay on the road with the same `ref`
or `name` value if available.
If there is no ambiguity or there are no `name` or `ref` values to disambiguate, then we pick the one with
the straightest following direction.
"""
# Find the indexes of the cosine of the deltas that are considered straight enough to continue.
idxs = np.nonzero(cos_delta < _ACCEPTABLE_BEARING_DELTA_COSINE)[0]
# If no amiguity or no way to break it, just return the straightest line.
if len(idxs) <= 1 or (last_wr.ref is None and last_wr.name is None):
# The section with the best continuation is the one with a bearing delta closest to pi. This is equivalent
# to taking the one with the smallest cosine of the bearing delta, as cosine is minimum (-1) on both pi
# and -pi.
return np.argmin(cos_delta)
wrs = [way_relations[idx] for idx in idxs]
# If we find a continuation way with the same reference we just choose it.
refs = list(map(lambda wr: wr.ref, wrs))
if last_wr.ref is not None:
idx = next((idx for idx, ref in enumerate(refs) if ref == last_wr.ref), None)
if idx is not None:
return idxs[idx]
# If we find a continuation way with the same name we just choose it.
names = list(map(lambda wr: wr.name, wrs))
if last_wr.name is not None:
idx = next((idx for idx, name in enumerate(names) if name == last_wr.name), None)
if idx is not None:
return idxs[idx]
# We did not manage to deambiguate, choose straightest path.
return np.argmin(cos_delta)
# Get the index of the continuation way.
best_idx = pick_best_idx(cos_delta)
# - Make sure to not select as route continuation a way that turns too much if we are close to the border of
# map data queried. This is to avoid building a route that takes a sharp turn just because we do not have the
# data for the way that actually continues straight.
if cos_delta[best_idx] > _MAX_ALLOWED_BEARING_DELTA_COSINE_AT_EDGE:
dist_to_center = distance_to_points(query_center, np.array([ref_point]))[0]
if dist_to_center > QUERY_RADIUS - _MAP_DATA_EDGE_DISTANCE:
break
# - Select next way.
last_wr = way_relations[best_idx]
except Exception as e:
print("Exception \"", str(e), "\" caught")
# Build the node data from the ordered list of way relations
self._nodes_data = NodesData(self._ordered_way_relations, wr_index)
# Locate where we are in the route node list.
self._locate()
def __repr__(self):
count = self._nodes_data.count if self._nodes_data is not None else None
return f'Route: {self.way_collection_id}, idx ahead: {self._ahead_idx} of {count}'
def _reset(self):
self._limits_ahead = None
self._cuvature_limits_ahead = None
self._curvatures_ahead = None
self._ahead_idx = None
self._distance_to_node_ahead = None
@property
def located(self):
return self._ahead_idx is not None
def _locate(self):
"""Will resolve the index in the nodes_data list for the node ahead of the current location.
It updates as well the distance from the current location to the node ahead.
"""
current = self.current_wr
if current is None:
return
node_ahead_id = current.node_ahead.id
self._distance_to_node_ahead = current.distance_to_node_ahead
start_idx = self._ahead_idx if self._ahead_idx is not None else 1
self._ahead_idx = None
ids = self._nodes_data.get(NodeDataIdx.node_id)
for idx in range(start_idx, len(ids)):
if ids[idx] == node_ahead_id:
self._ahead_idx = idx
break
@property
def current_wr(self):
return self._ordered_way_relations[0] if len(self._ordered_way_relations) else None
def update(self, location_rad, bearing_rad, location_stdev):
"""Will update the route structure based on the given `location_rad` and `bearing_rad` assuming progress on the
route on the original direction. If direction has changed or active point on the route can not be found, the route
will become invalid.
"""
if len(self._ordered_way_relations) == 0 or location_rad is None or bearing_rad is None:
return
# Skip if no update on location or bearing.
if np.array_equal(self.current_wr.location_rad, location_rad) and self.current_wr.bearing_rad == bearing_rad:
return
# Transverse the way relations on the actual order until we find an active one. From there, rebuild the route
# with the way relations remaining ahead.
for idx, wr in enumerate(self._ordered_way_relations):
active_direction = wr.direction
wr.update(location_rad, bearing_rad, location_stdev)
if not wr.active:
continue
if wr.direction != active_direction:
# Driving direction on the route has changed. stop.
break
# We have now the current wr. Repopulate from here till the end and locate
self._ordered_way_relations = self._ordered_way_relations[idx:]
self._reset()
self._locate()
# If the active way is diverting, check whether there are possibilities to divert from the route in the
# vecinity of the current location. If there are possibilities, then stop here to loose the route as we are
# most likely driving away. If there are no possibilities, then stick to the route as the diversion is probably
# just a matter of GPS accuracy. (It can happen after driving under a bridge)
if wr.diverting and len(self._nodes_data.possible_divertions(self._ahead_idx, self._distance_to_node_ahead)) > 0:
break
# The current location in route is valid, return.
return
# if we got here, there is no new active way relation or driving direction has changed. Reset.
self._reset()
@property
def speed_limits_ahead(self):
"""Returns and array of SpeedLimitSection objects for the actual route ahead of current location
"""
if self._limits_ahead is not None:
return self._limits_ahead
if self._nodes_data is None or self._ahead_idx is None:
return []
self._limits_ahead = self._nodes_data.speed_limits_ahead(self._ahead_idx, self._distance_to_node_ahead)
return self._limits_ahead
@property
def curvature_speed_limits_ahead(self):
"""Returns and array of TurnSpeedLimitSection objects for the actual route ahead of current location due
to curvatures
"""
if self._cuvature_limits_ahead is not None:
return self._cuvature_limits_ahead
if self._nodes_data is None or self._ahead_idx is None:
return []
self._cuvature_limits_ahead = self._nodes_data. \
curvatures_speed_limit_sections_ahead(self._ahead_idx, self._distance_to_node_ahead)
return self._cuvature_limits_ahead
@property
def current_speed_limit(self):
if not self.located:
return None
limits_ahead = self.speed_limits_ahead
if len(limits_ahead) == 0 or limits_ahead[0].start != 0:
return None
return limits_ahead[0].value
@property
def current_curvature_speed_limit_section(self):
if not self.located:
return None
limits_ahead = self.curvature_speed_limits_ahead
if len(limits_ahead) == 0 or limits_ahead[0].start != 0:
return None
return limits_ahead[0]
@property
def next_speed_limit_section(self):
if not self.located:
return None
limits_ahead = self.speed_limits_ahead
if len(limits_ahead) == 0:
return None
# Find the first section that does not start in 0. i.e. the next section
for section in limits_ahead:
if section.start > 0:
return section
return None
def next_curvature_speed_limit_sections(self, horizon_mts):
if not self.located:
return []
# Provide the curvature speed sections that start ahead (> 0) and up to horizon
return list(filter(lambda la: la.start > 0 and la.start <= horizon_mts, self.curvature_speed_limits_ahead))
@property
def distance_to_end(self):
if not self.located:
return None
return self._nodes_data.distance_to_end(self._ahead_idx, self._distance_to_node_ahead)
@property
def current_road_name(self):
return self.current_wr.road_name if self.located else None
-85
View File
@@ -1,85 +0,0 @@
from selfdrive.mapd.lib.WayRelation import WayRelation
from selfdrive.mapd.lib.WayRelationIndex import WayRelationIndex
from selfdrive.mapd.lib.Route import Route
from selfdrive.mapd.config import LANE_WIDTH
import uuid
_ACCEPTABLE_BEARING_DELTA_IND = 0.7071067811865475 # sin(pi/4) | 45 degrees acceptable bearing delta
class WayCollection():
"""A collection of WayRelations to use for maps data analysis.
"""
def __init__(self, areas, ways, query_center):
"""Creates a WayCollection with a set of OSM way objects.
Args:
ways (Array): Collection of Way objects fetched from OSM in a radius around `query_center`
query_center (Numpy Array): [lat, lon] numpy array in radians indicating the center of the data query.
"""
self.id = uuid.uuid4()
self.way_relations = [WayRelation(areas, way) for way in ways]
self.query_center = query_center
self.wr_index = WayRelationIndex(self.way_relations)
def get_route(self, location_rad, bearing_rad, location_stdev):
"""Provides the best route found in the way collection based on current location and bearing.
"""
if location_rad is None or bearing_rad is None or location_stdev is None:
return None
# Update all way relations in collection to the provided location and bearing.
for wr in self.way_relations:
wr.update(location_rad, bearing_rad, location_stdev)
# Get the way relations where a match was found. i.e. those now marked as active as long as the direction of
# travel is valid.
valid_way_relations = [wr for wr in self.way_relations if wr.active and not wr.is_prohibited]
# If no active, then we could not find a current way to build a route.
if len(valid_way_relations) == 0:
return None
# If only one valid, then pick it as current.
if len(valid_way_relations) == 1:
current = valid_way_relations[0]
# If more than one is valid, filter out any valid way relation where the bearing delta indicator is too high.
else:
wr_acceptable_bearing = list(filter(lambda wr: wr.active_bearing_delta <= _ACCEPTABLE_BEARING_DELTA_IND,
valid_way_relations))
# If delta bearing indicator is too high for all, then use as current the one that has the shorter one.
if len(wr_acceptable_bearing) == 0:
valid_way_relations.sort(key=lambda wr: wr.active_bearing_delta)
current = valid_way_relations[0]
# If only one with acceptable bearing, use it.
elif len(wr_acceptable_bearing) == 1:
current = wr_acceptable_bearing[0]
else:
# If more than one with acceptable bearing, filter the ones with distance to way lower than 2 standard
# deviation from GPS accuracy (95%) + half the road width estimate.
wr_accurate_distance = [wr for wr in wr_acceptable_bearing
if wr.distance_to_way <= 2. * location_stdev + wr.lanes * LANE_WIDTH / 2.]
# If none with accurate distance to way, then select the closest to the way
if len(wr_accurate_distance) == 0:
wr_acceptable_bearing.sort(key=lambda wr: wr.distance_to_way)
current = wr_acceptable_bearing[0]
# If only one with distance under accuracy, select this one.
elif len(wr_accurate_distance) == 1:
current = wr_accurate_distance[0]
# If more than one with distance under accuracy. Then select the one with lowest highway rank.
# i.e. preferred motorways over other roads and so on. This is to prevent selecting a small parallel
# road to a main road when the accuracy is poor.
else:
wr_accurate_distance.sort(key=lambda wr: wr.highway_rank)
current = wr_accurate_distance[0]
return Route(current, self.wr_index, self.id, self.query_center)
-487
View File
@@ -1,487 +0,0 @@
from selfdrive.mapd.lib.geo import DIRECTION, R, vectors, bearing_to_points, distance_to_points, point_on_line
from selfdrive.mapd.lib.osm import create_way
from common.conversions import Conversions as CV
from selfdrive.mapd.config import LANE_WIDTH
from common.basedir import BASEDIR
from datetime import datetime as dt
import numpy as np
import re
import json
_WAY_BBOX_PADING = 80. / R # 80 mts of padding to bounding box. (expressed in radians)
with open(BASEDIR + "/selfdrive/mapd/lib/default_speeds_by_region.json", "rb") as f:
DEFAULT_SPEEDS_BY_REGION = json.loads(f.read())
with open(BASEDIR + "/selfdrive/mapd/lib/default_speeds.json", "rb") as f:
_COUNTRY_LIMITS = json.loads(f.read())
_WD = {
'Mo': 0,
'Tu': 1,
'We': 2,
'Th': 3,
'Fr': 4,
'Sa': 5,
'Su': 6
}
_HIGHWAY_RANK = {
'motorway': 0,
'motorway_link': 1,
'trunk': 10,
'trunk_link': 11,
'primary': 20,
'primary_link': 21,
'secondary': 30,
'secondary_link': 31,
'tertiary': 40,
'tertiary_link': 41,
'unclassified': 50,
'residential': 60,
'living_street': 61,
'service': 62
}
def is_osm_time_condition_active(condition_string):
"""
Will indicate if a time condition for a restriction as described
@ https://wiki.openstreetmap.org/wiki/Conditional_restrictions
is active for the current date and time of day.
"""
now = dt.now().astimezone()
today = now.date()
week_days = []
# Look for days of week matched and validate if today matches criteria.
dr = re.findall(r'(Mo|Tu|We|Th|Fr|Sa|Su[-,\s]*?)', condition_string)
if len(dr) == 1:
week_days = [_WD[dr[0]]]
# If two or more matches condider it a range of days between 1st and 2nd element.
elif len(dr) > 1:
week_days = list(range(_WD[dr[0]], _WD[dr[1]] + 1))
# If valid week days list is not empty and today day is not in the list, then the time-date range is not active.
if len(week_days) > 0 and now.weekday() not in week_days:
return False
# Look for time ranges on the day. No time range, means all day
tr = re.findall(r'([0-9]{1,2}:[0-9]{2})\s*?-\s*?([0-9]{1,2}:[0-9]{2})', condition_string)
# if no time range but there were week days set, consider it active during the whole day
if len(tr) == 0:
return len(dr) > 0
# Search among time ranges matched, one where now time belongs too. If found range is active.
for times_tup in tr:
times = list(map(lambda tt: dt.
combine(today, dt.strptime(tt, '%H:%M').time().replace(tzinfo=now.tzinfo)), times_tup))
if now >= times[0] and now <= times[1]:
return True
return False
def speed_limit_value_for_limit_string(limit_string):
# Look for matches of speed by default in kph, or in mph when explicitly noted.
v = re.match(r'^\s*([0-9]{1,3})\s*?(mph)?\s*$', limit_string)
if v is None:
return None
conv = CV.MPH_TO_MS if v[2] is not None and v[2] == "mph" else CV.KPH_TO_MS
return conv * float(v[1])
def speed_limit_for_osm_tag_limit_string(limit_string):
# https://wiki.openstreetmap.org/wiki/Key:maxspeed
if limit_string is None:
# When limit is set to 0. is considered not existing.
return 0.
# Attempt to parse limit as simple numeric value considering units.
limit = speed_limit_value_for_limit_string(limit_string)
if limit is not None:
return limit
# Look for matches of speed with country implicit values.
v = re.match(r'^\s*([A-Z]{2}):([a-z_]+):?([0-9]{1,3})?(\s+)?(mph)?\s*', limit_string)
if v is None:
return 0.
if v[2] == "zone" and v[3] is not None:
conv = CV.MPH_TO_MS if v[5] is not None and v[5] == "mph" else CV.KPH_TO_MS
limit = conv * float(v[3])
elif f'{v[1]}:{v[2]}' in _COUNTRY_LIMITS:
limit = speed_limit_value_for_limit_string(_COUNTRY_LIMITS[f'{v[1]}:{v[2]}'])
return limit if limit is not None else 0.
def conditional_speed_limit_for_osm_tag_limit_string(limit_string):
if limit_string is None:
# When limit is set to 0. is considered not existing.
return 0.
# Look for matches of the `<restriction-value> @ (<condition>)` format
v = re.match(r'^(.*)@\s*\((.*)\).*$', limit_string)
if v is None:
return 0. # No valid format match
value = speed_limit_for_osm_tag_limit_string(v[1])
if value == 0.:
return 0. # Invalid speed limit value
# Look for date-time conditions separated by semicolon
v = re.findall(r'(?:;|^)([^;]*)', v[2])
for datetime_condition in v:
if is_osm_time_condition_active(datetime_condition):
return value
# If we get here, no current date-time condition is active.
return 0.
def speed_limit_value_for_highway_type(areas, tags):
max_speed = None
try:
geocode_country = ''
geocode_region = ''
for area in areas:
if area.tags.get('admin_level', '') == "2":
if area.tags.get('ISO3166-1:alpha2', '') != '':
geocode_country = area.tags.get('ISO3166-1:alpha2', '')
elif area.tags.get('admin_level', '') == "4":
geocode_region = area.tags.get('name', '')
country_rules = DEFAULT_SPEEDS_BY_REGION.get(geocode_country, {})
country_defaults = country_rules.get('Default', [])
for rule in country_defaults:
rule_valid = all(
tag_name in tags
and tags[tag_name] == value
for tag_name, value in rule['tags'].items()
)
if rule_valid:
max_speed = rule['speed']
break #stop searching country
region_rules = country_rules.get(geocode_region, [])
for rule in region_rules:
rule_valid = all(
tag_name in tags
and tags[tag_name] == value
for tag_name, value in rule['tags'].items()
)
if rule_valid:
max_speed = rule['speed']
break #stop searching region
except KeyError as e:
print(e)
except TypeError as e:
print(f"TypeError: {e} object is not iterable.")
if max_speed is None:
return 0
v = re.match(r'^\s*([0-9]{1,3})\s*?(mph)?\s*$', str(max_speed))
if v is None:
return None
conv = CV.MPH_TO_MS if v[2] is not None and v[2] == "mph" else CV.KPH_TO_MS
return conv * float(v[1])
class WayRelation():
"""A class that represent the relationship of an OSM way and a given `location` and `bearing` of a driving vehicle.
"""
def __init__(self, areas, way, parent=None):
self.way = way
self.areas = areas
self.parent = parent
self.parent_wr_id = parent.id if parent is not None else None # For WRs created as splits of other WRs
self.reset_location_variables()
self.direction = DIRECTION.NONE
self._speed_limit = None
self._advisory_speed_limit = None
self._one_way = way.tags.get("oneway")
self.name = way.tags.get('name')
self.ref = way.tags.get('ref')
self.highway_type = way.tags.get("highway")
self.highway_rank = _HIGHWAY_RANK.get(self.highway_type, 1000)
try:
self.lanes = int(way.tags.get('lanes'))
except Exception:
self.lanes = 2
# Create numpy arrays with nodes data to support calculations.
self._nodes_np = np.radians(np.array([[nd.lat, nd.lon] for nd in way.nodes], dtype=float))
self._nodes_ids = np.array([nd.id for nd in way .nodes], dtype=int)
# Get the vectors representation of the segments betwheen consecutive nodes. (N-1, 2)
v = vectors(self._nodes_np) * R
# Calculate the vector magnitudes (or distance) between nodes. (N-1)
self._way_distances = np.linalg.norm(v, axis=1)
# Calculate the bearing (from true north clockwise) for every section of the way (vectors between nodes). (N-1)
self._way_bearings = np.arctan2(v[:, 0], v[:, 1])
# Define bounding box to ease the process of locating a node in a way.
# [[min_lat, min_lon], [max_lat, max_lon]]
self.bbox = np.row_stack((np.amin(self._nodes_np, 0) - _WAY_BBOX_PADING,
np.amax(self._nodes_np, 0) + _WAY_BBOX_PADING))
# Get the edge nodes ids.
self.edge_nodes_ids = [way.nodes[0].id, way.nodes[-1].id]
def __repr__(self):
return f'(id: {self.id}, between {self.behind_idx} and {self.ahead_idx}, {self.direction}, active: {self.active})'
def __eq__(self, other):
if isinstance(other, WayRelation):
return self.id == other.id
return False
def reset_location_variables(self):
self.distance_to_node_ahead = 0.
self.location_rad = None
self.bearing_rad = None
self.active = False
self.diverting = False
self.ahead_idx = None
self.behind_idx = None
self._active_bearing_delta = None
self._distance_to_way = None
@property
def id(self):
return self.way.id
@property
def road_name(self):
if self.name is not None:
return self.name
return self.ref
def update(self, location_rad, bearing_rad, location_stdev):
"""Will update and validate the associated way with a given `location_rad` and `bearing_rad`.
Specifically it will find the nodes behind and ahead of the current location and bearing.
If no proper fit to the way geometry, the way relation is marked as invalid.
"""
self.reset_location_variables()
# Ignore if location not in way bounding box
if not self.is_location_in_bbox(location_rad):
return
# - Get the distance and bearings from location to all nodes. (N)
bearings = bearing_to_points(location_rad, self._nodes_np)
# - Get absolute bearing delta to current driving bearing. (N)
delta = np.abs(bearing_rad - bearings)
# - Nodes are ahead if the cosine of the delta is positive (N)
is_ahead = np.cos(delta) >= 0.
# - Possible locations on the way are those where adjacent nodes change from ahead to behind or vice-versa.
possible_idxs = np.nonzero(np.diff(is_ahead))[0]
# - when no possible locations found, then the location is not in this way.
if len(possible_idxs) == 0:
return
projections = point_on_line(self._nodes_np[:-1], self._nodes_np[1:], location_rad)
h = distance_to_points(location_rad, projections)
# - Calculate the delta between driving bearing and way bearings. (N-1)
bw_delta = self._way_bearings - bearing_rad
# - The absolute value of the sin of `bw_delta` indicates how close the bearings match independent of direction.
# We will use this value along the distance to the way to aid on way selection. (N-1)
abs_sin_bw_delta = np.abs(np.sin(bw_delta))
# - Get the delta to way bearing indicators and the distance to the way for the possible locations.
abs_sin_bw_delta_possible = abs_sin_bw_delta[possible_idxs]
h_possible = h[possible_idxs]
# - Get the index where the distance to the way is minimum. That is the chosen location.
min_h_possible_idx = np.argmin(h_possible)
min_delta_idx = possible_idxs[min_h_possible_idx]
projection = projections[min_delta_idx]
# - If the distance to the way is over 4 standard deviations of the gps accuracy + the maximum road width
# estimate, then we are way too far to stick to this way (i.e. we are not on this way anymore)
# In theory the osm path is centered on the road which means half the road width would cover the whole road.
# however, often times the osm path is not perfectly centered so we'll make the possible route more lenient by using
# the full road width.
road_width_estimate = self.lanes * LANE_WIDTH
half_road_width_estimate = road_width_estimate / 2.
if h_possible[min_h_possible_idx] > 4. * location_stdev + road_width_estimate:
return
# If the distance to the road is greater than 2 standard deviations of the gps accuracy + half the maximum road
# width estimate + 1 lane width then we are most likely diverting from this route. Adding a lane width to give
# leniency to not perfectly centered osm paths
diverting = h_possible[min_h_possible_idx] > 2. * location_stdev + half_road_width_estimate + LANE_WIDTH
# Populate location variables with result
if is_ahead[min_delta_idx]:
self.direction = DIRECTION.BACKWARD
self.ahead_idx = min_delta_idx
self.behind_idx = min_delta_idx + 1
else:
self.direction = DIRECTION.FORWARD
self.ahead_idx = min_delta_idx + 1
self.behind_idx = min_delta_idx
self._distance_to_way = h[min_delta_idx]
self._active_bearing_delta = abs_sin_bw_delta_possible[min_h_possible_idx]
# find the distance to the next node by projecting our location onto the line and finding the delta between that
# point and the next point on the route
self.distance_to_node_ahead = distance_to_points(projection, np.array([self._nodes_np[self.ahead_idx]]))[0]
self.active = True
self.diverting = diverting
self.location_rad = location_rad
self.bearing_rad = bearing_rad
self._speed_limit = None
self._advisory_speed_limit = None
def update_direction_from_starting_node(self, start_node_id):
self._speed_limit = None
self._advisory_speed_limit = None
if self.edge_nodes_ids[0] == start_node_id:
self.direction = DIRECTION.FORWARD
elif self.edge_nodes_ids[-1] == start_node_id:
self.direction = DIRECTION.BACKWARD
else:
self.direction = DIRECTION.NONE
def is_location_in_bbox(self, location_rad):
"""Indicates if a given location is contained in the bounding box surrounding the way.
self.bbox = [[min_lat, min_lon], [max_lat, max_lon]]
"""
is_g = np.greater_equal(location_rad, self.bbox[0, :])
is_l = np.less_equal(location_rad, self.bbox[1, :])
return np.all(np.concatenate((is_g, is_l)))
@property
def speed_limit(self):
if self._speed_limit is not None:
return self._speed_limit
# Get string from corresponding tag, consider conditional limits first.
limit_string = self.way.tags.get("maxspeed:conditional")
if limit_string is None:
if self.direction == DIRECTION.FORWARD:
limit_string = self.way.tags.get("maxspeed:forward:conditional")
elif self.direction == DIRECTION.BACKWARD:
limit_string = self.way.tags.get("maxspeed:backward:conditional")
limit = conditional_speed_limit_for_osm_tag_limit_string(limit_string)
# When no conditional limit set, attempt to get from regular speed limit tags.
if limit == 0.:
limit_string = self.way.tags.get("maxspeed")
if limit_string is None:
if self.direction == DIRECTION.FORWARD:
limit_string = self.way.tags.get("maxspeed:forward")
elif self.direction == DIRECTION.BACKWARD:
limit_string = self.way.tags.get("maxspeed:backward")
limit = speed_limit_for_osm_tag_limit_string(limit_string)
if limit == 0.:
limit = speed_limit_value_for_highway_type(self.areas, self.way.tags)
self._speed_limit = limit
return self._speed_limit
@property
def advisory_speed_limit(self):
if self._advisory_speed_limit is not None:
return self._advisory_speed_limit
limit_string = self.way.tags.get("maxspeed:advisory")
limit = speed_limit_for_osm_tag_limit_string(limit_string)
self._advisory_speed_limit = limit
return self._advisory_speed_limit
@property
def active_bearing_delta(self):
"""Returns the sine of the delta between the current location bearing and the exact
bearing of the portion of way we are currentluy located at.
"""
return self._active_bearing_delta
@property
def is_one_way(self):
return self._one_way in ['yes'] or self.highway_type in ["motorway"]
@property
def is_prohibited(self):
# Direction must be defined to asses this property. Default to `True` if not.
if self.direction == DIRECTION.NONE:
return True
return self.is_one_way and self.direction == DIRECTION.BACKWARD
@property
def distance_to_way(self):
"""Returns the perpendicular (i.e. minimum) distance between current location and the way
"""
return self._distance_to_way
@property
def node_ahead(self):
return self.way.nodes[self.ahead_idx] if self.ahead_idx is not None else None
@property
def last_node(self):
"""Returns the last node on the way considering the traveling direction
"""
if self.direction == DIRECTION.FORWARD:
return self.way.nodes[-1]
if self.direction == DIRECTION.BACKWARD:
return self.way.nodes[0]
return None
@property
def last_node_coordinates(self):
"""Returns the coordinates for the last node on the way considering the traveling direction. (in radians)
"""
if self.direction == DIRECTION.FORWARD:
return self._nodes_np[-1]
if self.direction == DIRECTION.BACKWARD:
return self._nodes_np[0]
return None
def node_before_edge_coordinates(self, node_id):
"""Returns the coordinates of the node before the edge node identifeid with `node_id`. (in radians)
"""
if self.edge_nodes_ids[0] == node_id:
return self._nodes_np[1]
if self.edge_nodes_ids[-1] == node_id:
return self._nodes_np[-2]
return np.array([0., 0.])
def split(self, node_id, way_ids=None):
""" Returns and array with the way relations resulting from splitting the current way relation at node_id
"""
idxs = np.nonzero(self._nodes_ids == node_id)[0]
if len(idxs) == 0:
return []
idx = idxs[0]
if idx == 0 or idx == len(self._nodes_ids) - 1:
return [self]
if not isinstance(way_ids, list):
way_ids = [-1, -2] # Default id values.
ways = [create_way(way_ids[0], node_ids=self._nodes_ids[:idx + 1], from_way=self.way),
create_way(way_ids[1], node_ids=self._nodes_ids[idx:], from_way=self.way)]
return [WayRelation(self.areas, way, parent=self) for way in ways]
-34
View File
@@ -1,34 +0,0 @@
class WayRelationIndex():
"""
A class containing an index of WayRelations by node ids of internal nodes and edge nodes.
"""
def __init__(self, way_relations):
self._edge_nodes_index_dict = {}
self._full_nodes_index_dict = {}
for wr in way_relations:
self.add(wr)
def add(self, way_relation):
for node in way_relation.way.nodes:
node_id = node.id
self._full_nodes_index_dict[node_id] = self._full_nodes_index_dict.get(node_id, []) + [way_relation]
if node_id in way_relation.edge_nodes_ids:
self._edge_nodes_index_dict[node_id] = self._edge_nodes_index_dict.get(node_id, []) + [way_relation]
def remove(self, way_relation):
for node in way_relation.way.nodes:
node_id = node.id
self._full_nodes_index_dict[node_id] = [wr for wr in self._full_nodes_index_dict.get(node_id, [])
if wr is not way_relation]
if node_id in way_relation.edge_nodes_ids:
self._edge_nodes_index_dict[node_id] = [wr for wr in self._edge_nodes_index_dict.get(node_id, [])
if wr is not way_relation]
def way_relations_with_edge_node_id(self, node_id):
return self._edge_nodes_index_dict.get(node_id, [])
def way_relations_with_node_id(self, node_id):
return self._full_nodes_index_dict.get(node_id, [])
-112
View File
@@ -1,112 +0,0 @@
{
"_comment": "These speeds are from https://wiki.openstreetmap.org/wiki/Speed_limits Special cases have been stripped",
"AR:urban": "40",
"AR:urban:primary": "60",
"AR:urban:secondary": "60",
"AR:rural": "110",
"AT:urban": "50",
"AT:rural": "100",
"AT:trunk": "100",
"AT:motorway": "130",
"BE:urban": "50",
"BE-VLG:rural": "70",
"BE-WAL:rural": "90",
"BE:trunk": "120",
"BE:motorway": "120",
"CH:urban[1]": "50",
"CH:rural": "80",
"CH:trunk": "100",
"CH:motorway": "120",
"CZ:pedestrian_zone": "20",
"CZ:living_street": "20",
"CZ:urban": "50",
"CZ:urban_trunk": "80",
"CZ:urban_motorway": "80",
"CZ:rural": "90",
"CZ:trunk": "110",
"CZ:motorway": "130",
"DK:urban": "50",
"DK:rural": "80",
"DK:motorway": "130",
"DE:living_street": "10",
"DE:service": "10",
"DE:residential": "30",
"DE:urban": "50",
"DE:rural": "100",
"DE:trunk": "none",
"DE:motorway": "none",
"FI:urban": "50",
"FI:rural": "80",
"FI:trunk": "100",
"FI:motorway": "120",
"FR:urban": "50",
"FR:rural": "80",
"FR:trunk": "110",
"FR:motorway": "130",
"GR:urban": "50",
"GR:rural": "90",
"GR:trunk": "110",
"GR:motorway": "130",
"HU:urban": "50",
"HU:rural": "90",
"HU:trunk": "110",
"HU:motorway": "130",
"IT:urban": "50",
"IT:rural": "90",
"IT:trunk": "110",
"IT:motorway": "130",
"JP:national": "60",
"JP:motorway": "100",
"LT:living_street": "20",
"LT:urban": "50",
"LT:rural": "90",
"LT:trunk": "120",
"LT:motorway": "130",
"PL:living_street": "20",
"PL:urban": "50",
"PL:rural": "90",
"PL:trunk": "100",
"PL:motorway": "140",
"RO:urban": "50",
"RO:rural": "90",
"RO:trunk": "100",
"RO:motorway": "130",
"RU:living_street": "20",
"RU:urban": "60",
"RU:rural": "90",
"RU:motorway": "110",
"SK:urban": "50",
"SK:rural": "90",
"SK:trunk": "90",
"SK:motorway": "90",
"SI:urban": "50",
"SI:rural": "90",
"SI:trunk": "110",
"SI:motorway": "130",
"ES:living_street": "20",
"ES:urban": "50",
"ES:rural": "50",
"ES:trunk": "90",
"ES:motorway": "120",
"SE:urban": "50",
"SE:rural": "70",
"SE:trunk": "90",
"SE:motorway": "110",
"GB:nsl_restricted": "30 mph",
"GB:nsl_single": "60 mph",
"GB:nsl_dual": "70 mph",
"GB:motorway": "70 mph",
"UA:urban": "50",
"UA:rural": "90",
"UA:trunk": "110",
"UA:motorway": "130",
"UZ:living_street": "30",
"UZ:urban": "70",
"UZ:rural": "100",
"UZ:motorway": "110",
"ZA:trunk": "120",
"ZA:residential": "60",
"ZA:rural": "100",
"ZA:urban": "60",
"ZA:motorway": "120"
}
@@ -1,624 +0,0 @@
{
"AU": {
"Default": [
{
"speed": "100",
"tags": {
"highway": "motorway"
}
},
{
"speed": "80",
"tags": {
"highway": "trunk"
}
},
{
"speed": "80",
"tags": {
"highway": "primary"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "80",
"tags": {
"highway": "unclassified"
}
},
{
"speed": "50",
"tags": {
"highway": "residential"
}
},
{
"speed": "40",
"tags": {
"highway": "service"
}
},
{
"speed": "90",
"tags": {
"highway": "motorway_link"
}
},
{
"speed": "80",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "80",
"tags": {
"highway": "primary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary_link"
}
},
{
"speed": "30",
"tags": {
"highway": "living_street"
}
}
]
},
"EE": {
"Default": [
{
"speed": "90",
"tags": {
"highway": "motorway"
}
},
{
"speed": "90",
"tags": {
"highway": "trunk"
}
},
{
"speed": "90",
"tags": {
"highway": "primary"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "90",
"tags": {
"highway": "unclassified"
}
},
{
"speed": "40",
"tags": {
"highway": "residential"
}
},
{
"speed": "40",
"tags": {
"highway": "service"
}
},
{
"speed": "90",
"tags": {
"highway": "motorway_link"
}
},
{
"speed": "70",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "70",
"tags": {
"highway": "primary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary_link"
}
},
{
"speed": "20",
"tags": {
"highway": "living_street"
}
}
]
},
"CA": {
"Default": [
{
"speed": "100",
"tags": {
"highway": "motorway"
}
},
{
"speed": "80",
"tags": {
"highway": "trunk"
}
},
{
"speed": "80",
"tags": {
"highway": "primary"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "80",
"tags": {
"highway": "unclassified"
}
},
{
"speed": "40",
"tags": {
"highway": "residential"
}
},
{
"speed": "40",
"tags": {
"highway": "service"
}
},
{
"speed": "90",
"tags": {
"highway": "motorway_link"
}
},
{
"speed": "80",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "80",
"tags": {
"highway": "primary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "50",
"tags": {
"highway": "tertiary_link"
}
},
{
"speed": "20",
"tags": {
"highway": "living_street"
}
}
]
},
"DE": {
"Default": [
{
"speed": "none",
"tags": {
"highway": "motorway"
}
},
{
"speed": "10",
"tags": {
"highway": "living_street"
}
},
{
"speed": "30",
"tags": {
"highway": "residential"
}
},
{
"speed": "10",
"tags": {
"highway": "service"
}
},
{
"speed": "100",
"tags": {
"zone:traffic": "DE:rural"
}
},
{
"speed": "50",
"tags": {
"zone:traffic": "DE:urban"
}
},
{
"speed": "30",
"tags": {
"zone:maxspeed": "DE:30"
}
},
{
"speed": "50",
"tags": {
"zone:maxspeed": "DE:urban"
}
},
{
"speed": "100",
"tags": {
"zone:maxspeed": "DE:rural"
}
},
{
"speed": "none",
"tags": {
"zone:maxspeed": "DE:motorway"
}
},
{
"speed": "30",
"tags": {
"bicycle_road": "yes"
}
}
]
},
"ZA": {
"Default": [
{
"speed": "120",
"tags": {
"highway": "motorway"
}
},
{
"speed": "120",
"tags": {
"highway": "motorway_link"
}
},
{
"speed": "100",
"tags": {
"highway": "primary"
}
},
{
"speed": "100",
"tags": {
"highway": "secondary"
}
},
{
"speed": "60",
"tags": {
"highway": "residential"
}
}
]
},
"US": {
"South Dakota": [
{
"speed": "80 mph",
"tags": {
"highway": "motorway"
}
},
{
"speed": "70 mph",
"tags": {
"highway": "trunk"
}
},
{
"speed": "65 mph",
"tags": {
"highway": "primary"
}
},
{
"speed": "70 mph",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "65 mph",
"tags": {
"highway": "primary_link"
}
}
],
"Wisconsin": [
{
"speed": "65 mph",
"tags": {
"highway": "trunk"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "35 mph",
"tags": {
"highway": "unclassified"
}
},
{
"speed": "65 mph",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "tertiary_link"
}
}
],
"Default": [
{
"speed": "65 mph",
"tags": {
"highway": "motorway"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "trunk"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "primary"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "secondary"
}
},
{
"speed": "35 mph",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "unclassified"
}
},
{
"speed": "25 mph",
"tags": {
"highway": "residential"
}
},
{
"speed": "25 mph",
"tags": {
"highway": "service"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "motorway_link"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "trunk_link"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "primary_link"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "35 mph",
"tags": {
"highway": "tertiary_link"
}
},
{
"speed": "15 mph",
"tags": {
"highway": "living_street"
}
}
],
"Michigan": [
{
"speed": "70 mph",
"tags": {
"highway": "motorway"
}
}
],
"Oregon": [
{
"speed": "55 mph",
"tags": {
"highway": "motorway"
}
},
{
"speed": "35 mph",
"tags": {
"highway": "secondary"
}
},
{
"speed": "30 mph",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "15 mph",
"tags": {
"highway": "service"
}
},
{
"speed": "35 mph",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "30 mph",
"tags": {
"highway": "tertiary_link"
}
}
],
"New York": [
{
"speed": "65 mph",
"tags": {
"highway": "motorway"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "primary"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "secondary"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "tertiary"
}
},
{
"speed": "30 mph",
"tags": {
"highway": "residential"
}
},
{
"speed": "45 mph",
"tags": {
"highway": "primary_link"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "secondary_link"
}
},
{
"speed": "55 mph",
"tags": {
"highway": "tertiary_link"
}
},
{
"speed": "20 mph",
"tags": {
"highway": "living_street"
}
}
]
}
}
-78
View File
@@ -1,78 +0,0 @@
from enum import Enum
import numpy as np
R = 6373000.0 # approximate radius of earth in mts
def vectors(points):
"""Provides a array of vectors on cartesian space (x, y).
Each vector represents the path from a point in `points` to the next.
`points` must by a (N, 2) array of [lat, lon] pairs in radians.
"""
latA = points[:-1, 0]
latB = points[1:, 0]
delta = np.diff(points, axis=0)
dlon = delta[:, 1]
x = np.sin(dlon) * np.cos(latB)
y = np.cos(latA) * np.sin(latB) - (np.sin(latA) * np.cos(latB) * np.cos(dlon))
return np.column_stack((x, y))
def ref_vectors(ref, points):
"""Provides a array of vectors on cartesian space (x, y).
Each vector represents the path from ref to a point in `points`.
`points` must by a (N, 2) array of [lat, lon] pairs in radians.
"""
latA = ref[0]
latB = points[:, 0]
delta = points - ref
dlon = delta[:, 1]
x = np.sin(dlon) * np.cos(latB)
y = np.cos(latA) * np.sin(latB) - (np.sin(latA) * np.cos(latB) * np.cos(dlon))
return np.column_stack((x, y))
def bearing_to_points(point, points):
"""Calculate the bearings (angle from true north clockwise) of the vectors between `point` and each
one of the entries in `points`. Both `point` and `points` elements are 2 element arrays containing a latitud,
longitude pair in radians.
"""
delta = points - point
x = np.sin(delta[:, 1]) * np.cos(points[:, 0])
y = np.cos(point[0]) * np.sin(points[:, 0]) - (np.sin(point[0]) * np.cos(points[:, 0]) * np.cos(delta[:, 1]))
return np.arctan2(x, y)
def point_on_line(start_points, end_points, point, extend_line = False):
"""project a single point onto each line for an np array of start points and end points
ref: https://stackoverflow.com/a/61342198
"""
ap = np.subtract(point, start_points)
ab = np.subtract(end_points, start_points)
t = np.array([np.dot(ap[i], ab[i]) / np.dot(ab[i], ab[i]) for i in range(len(ap))])
# if you need the the closest point belonging to the segment
if not extend_line:
t = np.maximum(0, np.minimum(1, t))
result = np.add(start_points, np.array([t[i] * ab[i] for i in range(len(t))]))
return result
def distance_to_points(point, points):
"""Calculate the distance of the vectors between `point` and each one of the entries in `points`.
Both `point` and `points` elements are 2 element arrays containing a latitud, longitude pair in radians.
"""
delta = points - point
a = np.sin(delta[:, 0] / 2)**2 + np.cos(point[0]) * np.cos(points[:, 0]) * np.sin(delta[:, 1] / 2)**2
c = 2 * np.arctan2(np.sqrt(a), np.sqrt(1 - a))
return c * R
class DIRECTION(Enum):
NONE = 0
AHEAD = 1
BEHIND = 2
FORWARD = 3
BACKWARD = 4
-102
View File
@@ -1,102 +0,0 @@
import overpy
import subprocess
import email.utils as eut
import time
from common.params import Params
from system.version import get_version
from urllib.request import Request, urlopen
OSM_LOCAL_PATH = "/data/media/0/osm"
OSM_DB_STAMP_FILE = OSM_LOCAL_PATH + "/db_stamp"
OSM_QUERY = [f"{OSM_LOCAL_PATH}/v0.7.57/bin/osm3s_query", f"--db-dir={OSM_LOCAL_PATH}/db"]
OSM_DB_STAMP_REMOTE = "https://sunnypilot-osm.s3.us-east-2.amazonaws.com/osm-db/timestamps"
def get_current_s3_osm_db_timestamp():
try:
local_osm_db_name = Params().get("OsmLocationName", encoding="utf8")
req = Request(url=f"{OSM_DB_STAMP_REMOTE}/{local_osm_db_name}.txt", headers={"User-Agent": f"sunnypilot-{get_version()}"})
r = urlopen(req)
if r.status != 200:
print(f'Failed to fetch timestamp for S3 OSM db.\n\n{r.status}')
return None
timestamp_string = r.read().decode("utf-8").strip()
if timestamp_string is None:
print('Timestamp file for S3 OSM db contained no value.')
return None
parsed_date = eut.parsedate(timestamp_string)
return time.mktime(parsed_date)
except Exception as e:
print(f'Could not parse timestamp for S3 local osm db.\n\n{e}')
return None
def persist_s3_osm_db_timestamp(timestamp):
try:
with open(OSM_DB_STAMP_FILE, 'w') as file:
file.write(f'{timestamp}')
except Exception as e:
print(f'Failed to timestamp local OSM db.\n\n{e}')
def get_local_osm_timestamp():
try:
with open(OSM_DB_STAMP_FILE, 'r') as file:
return float(file.readline())
except Exception as e:
print(f'Failed to read timestamp for local OSM db.\n\n{e}')
return None
def is_osm_db_up_to_date():
current_osm_ts = get_local_osm_timestamp()
if current_osm_ts is None:
return False
current_s3_osm_ts = get_current_s3_osm_db_timestamp()
if current_s3_osm_ts is None:
return True
return current_osm_ts == current_s3_osm_ts
def timestamp_local_osm_db():
current_s3_osm_ts = get_current_s3_osm_db_timestamp()
if current_s3_osm_ts is not None:
persist_s3_osm_db_timestamp(current_s3_osm_ts)
def is_local_osm_installed(params=Params()):
api = overpy.Overpass()
waypoint = params.get("OsmWayTest", encoding="utf8")
if waypoint is None:
return False
q = f"""
way({waypoint});
(._;>;);
out;
"""
try:
completion = subprocess.run(OSM_QUERY + [f"--request={q}"], check=True, capture_output=True)
print(f'OSM local query returned with exit code: {completion.returncode}')
if completion.returncode != 0:
return False
print(f'OSM Local query returned:\n\n{completion.stdout}')
ways = api.parse_xml(completion.stdout).ways
success = len(ways) == 1
print(f"Test osm script returned {len(ways)} ways")
print(f'OSM local server query {"succeeded" if success else "failed"}')
return success
except Exception as e:
print(e)
return False
-71
View File
@@ -1,71 +0,0 @@
import overpy
import subprocess
import numpy as np
from cereal import custom
from common.params import Params
from selfdrive.mapd.lib.geo import R
from selfdrive.mapd.lib.helpers import is_local_osm_installed, OSM_QUERY
DataType = custom.LiveMapDataSP.DataType
def create_way(way_id, node_ids, from_way):
"""
Creates and OSM Way with the given `way_id` and list of `node_ids`, copying attributes and tags from `from_way`
"""
return overpy.Way(way_id, node_ids=node_ids, attributes={}, result=from_way._result,
tags=from_way.tags)
class OSM:
def __init__(self):
self.api = overpy.Overpass()
self.param_s = Params()
self._osm_local_db_enabled = self.param_s.get_bool("OsmLocalDb")
self._local_osm_installed = is_local_osm_installed(self.param_s)
# self.api = overpy.Overpass(url='http://3.65.170.21/api/interpreter')
def _online_query(self, q, area_q):
print("Query OSM from remote server")
query = self.api.query(q + area_q)
areas, ways = query.areas, query.ways
data_type = DataType.online
return areas, ways, data_type
def fetch_road_ways_around_location(self, lat, lon, radius):
# Calculate the bounding box coordinates for the bbox containing the circle around location.
bbox_angle = np.degrees(radius / R)
# fetch all ways and nodes on this ways in bbox
bbox_str = f'{str(lat - bbox_angle)},{str(lon - bbox_angle)},{str(lat + bbox_angle)},{str(lon + bbox_angle)}'
lat_lon = "(%f,%f)" % (lat, lon)
q = """
way(""" + bbox_str + """)
[highway]
[highway!~"^(footway|path|corridor|bridleway|steps|cycleway|construction|bus_guideway|escape|service|track)$"];
(._;>;);
out;"""
area_q = """is_in""" + lat_lon + """;area._[admin_level~"[24]"];
convert area ::id = id(), admin_level = t['admin_level'],
name = t['name'], "ISO3166-1:alpha2" = t['ISO3166-1:alpha2'];out;
"""
try:
if self._osm_local_db_enabled and self._local_osm_installed:
print("Query OSM from local server")
completion = subprocess.run(OSM_QUERY + [f"--request={q}"], check=True, capture_output=True)
ways = self.api.parse_xml(completion.stdout).ways
if completion.returncode == 0 and len(ways) != 0:
try:
areas = self.api.query(area_q).areas
except Exception as e:
print(f'Exception while querying "AREAS" OSM from local server:\n{e}')
areas = None
data_type = DataType.offline
else:
areas, ways, data_type = self._online_query(q, area_q)
else:
areas, ways, data_type = self._online_query(q, area_q)
except Exception as e:
print(f'Exception while querying OSM:\n{e}')
areas, ways, data_type = [], [], DataType.default
return areas, ways, data_type
-294
View File
@@ -1,294 +0,0 @@
#!/usr/bin/env python3
import json
import threading
from traceback import print_exception
import numpy as np
from time import strftime, gmtime
from cereal import custom
import cereal.messaging as messaging
from common.params import Params
from common.realtime import set_core_affinity, set_thread_affinity, Ratekeeper
from selfdrive.mapd.lib.osm import OSM
from selfdrive.mapd.lib.geo import distance_to_points
from selfdrive.mapd.lib.WayCollection import WayCollection
from selfdrive.mapd.config import QUERY_RADIUS, QUERY_RADIUS_OFFLINE, MIN_DISTANCE_FOR_NEW_QUERY, FULL_STOP_MAX_SPEED, LOOK_AHEAD_HORIZON_TIME
from system.swaglog import cloudlog
DataType = custom.LiveMapDataSP.DataType
_DEBUG = False
_CLOUDLOG_DEBUG = True
ROAD_NAME_TIMEOUT = 30 # secs
def _debug(msg, log_to_cloud=True):
if _CLOUDLOG_DEBUG and log_to_cloud:
cloudlog.debug(msg)
if _DEBUG:
print(msg)
def excepthook(args):
_debug(f'MapD: Threading exception:\n{args}')
print_exception(args.exc_type, args.exc_value, args.exc_traceback)
threading.excepthook = excepthook
class MapD():
def __init__(self):
self.osm = OSM()
self.way_collection = None
self.route = None
self.last_gps_fix_timestamp = 0
self.last_gps = None
self.location_deg = None # The current location in degrees.
self.location_rad = None # The current location in radians as a Numpy array.
self.bearing_rad = None
self.location_stdev = None # The current location accuracy in mts. 1 standard devitation.
self.gps_speed = 0.
self.last_fetch_location = None
self.last_route_update_fix_timestamp = 0
self.last_publish_fix_timestamp = 0
self.data_type = DataType.default
self._op_enabled = False
self._disengaging = False
self._query_thread = None
self._lock = threading.RLock()
self.gps_sock = 'gpsLocationExternal'
# dp - use LastGPSPosition as init position (if we are in a undercover car park?)
# this way we can prefetch osm data before we get a fix.
last_pos = Params().get("LastGPSPosition")
if last_pos is not None and last_pos != "":
l = json.loads(last_pos)
lat = float(l["latitude"])
lon = float(l["longitude"])
self.location_rad = np.radians(np.array([lat, lon], dtype=float))
self.location_deg = (lat, lon)
self.bearing_rad = np.radians(0, dtype=float)
_debug("Use LastGPSPosition position - lat: %s, lon: %s" % (lat, lon))
def udpate_state(self, sm):
sock = 'carControl'
if not sm.updated[sock] or not sm.valid[sock]:
return
hud_control = sm[sock].hudControl
self._disengaging = not hud_control.speedVisible and self._op_enabled
self._op_enabled = hud_control.speedVisible
def update_gps(self, sm):
self.gps_sock = 'gpsLocationExternal' if sm.rcv_frame['gpsLocationExternal'] > 1 else 'gpsLocation'
if not sm.updated[self.gps_sock] or not sm.valid[self.gps_sock]:
return
log = sm[self.gps_sock]
self.last_gps = log
# ignore the message if the fix is invalid
if log.flags % 2 == 0:
return
self.last_gps_fix_timestamp = log.unixTimestampMillis # Unix TS. Milliseconds since January 1, 1970.
self.location_rad = np.radians(np.array([log.latitude, log.longitude], dtype=float))
self.location_deg = (log.latitude, log.longitude)
self.bearing_rad = np.radians(log.bearingDeg, dtype=float)
self.gps_speed = log.speed
self.location_stdev = log.accuracy if self.gps_sock == 'gpsLocationExternal' else 1 # gpsLocation doesn't report accuracy
_debug('Mapd: ********* Got GPS fix'
+ f'Pos: {self.location_deg} +/- {self.location_stdev * 2.} mts.\n'
+ f'Bearing: {log.bearingDeg} +/- {log.bearingAccuracyDeg * 2.} deg.\n'
+ f'timestamp: {strftime("%d-%m-%y %H:%M:%S", gmtime(self.last_gps_fix_timestamp * 1e-3))}'
+ '*******', log_to_cloud=False)
def _query_osm_not_blocking(self):
def query(osm, location_deg, location_rad, radius):
_debug(f'Mapd: Start query for OSM map data at {location_deg}')
lat, lon = location_deg
areas, ways, self.data_type = osm.fetch_road_ways_around_location(lat, lon, radius)
_debug(f'Mapd: Query to OSM finished with {len(ways)} ways')
# Only issue an update if we received some ways. Otherwise it is most likely a connectivity issue.
# Will retry on next loop.
if len(ways) > 0:
new_way_collection = WayCollection(areas, ways, location_rad)
# Use the lock to update the way_collection as it might be being used to update the route.
_debug('Mapd: Locking to write results from osm.', log_to_cloud=False)
with self._lock:
self.way_collection = new_way_collection
self.last_fetch_location = location_rad
_debug(f'Mapd: Updated map data @ {location_deg} - got {len(ways)} ways')
_debug('Mapd: Releasing Lock to write results from osm', log_to_cloud=False)
# Ignore if we have a query thread already running.
if self._query_thread is not None and self._query_thread.is_alive():
return
self._query_thread = threading.Thread(target=query, args=(self.osm, self.location_deg, self.location_rad,
QUERY_RADIUS_OFFLINE if self.data_type == DataType.offline else QUERY_RADIUS))
set_thread_affinity(self._query_thread, [0, 1, 2, 3])
self._query_thread.start()
def updated_osm_data(self):
if self.route is not None:
distance_to_end = self.route.distance_to_end
if distance_to_end is not None and distance_to_end >= MIN_DISTANCE_FOR_NEW_QUERY:
# do not query as long as we have a route with enough distance ahead.
return
if self.location_rad is None:
return
if self.last_fetch_location is not None:
distance_since_last = distance_to_points(self.last_fetch_location, np.array([self.location_rad]))[0]
if distance_since_last < (QUERY_RADIUS_OFFLINE if self.data_type == DataType.offline else QUERY_RADIUS) - MIN_DISTANCE_FOR_NEW_QUERY:
# do not query if are still not close to the border of previous query area
return
self._query_osm_not_blocking()
def update_route(self):
def update_proc():
# Ensure we clear the route on op disengage, this way we can correct possible incorrect map data due
# to wrongly locating or picking up the wrong route.
if self._disengaging:
self.route = None
_debug('Mapd *****: Clearing Route as system is disengaging. ********')
if self.way_collection is None or self.location_rad is None or self.bearing_rad is None:
_debug('Mapd *****: Can not update route. Missing WayCollection, location or bearing ********')
return
if self.route is not None and self.last_route_update_fix_timestamp == self.last_gps_fix_timestamp:
_debug('Mapd *****: Skipping route update. No new fix since last update ********')
return
self.last_route_update_fix_timestamp = self.last_gps_fix_timestamp
# Create the route if not existent or if it was generated by an older way collection
if self.route is None or self.route.way_collection_id != self.way_collection.id:
self.route = self.way_collection.get_route(self.location_rad, self.bearing_rad, self.location_stdev)
_debug(f'Mapd *****: Route created: \n{self.route}\n********')
return
# Do not attempt to update the route if the car is going close to a full stop, as the bearing can start
# jumping and creating unnecessary losing of the route. Since the route update timestamp has been updated
# a new liveMapDataSP message will be published with the current values (which is desirable)
if self.gps_speed < FULL_STOP_MAX_SPEED:
_debug('Mapd *****: Route Not updated as car has Stopped ********')
return
self.route.update(self.location_rad, self.bearing_rad, self.location_stdev)
if self.route.located:
_debug(f'Mapd *****: Route updated: \n{self.route}\n********')
return
# if an old route did not mange to locate, attempt to regenerate form way collection.
self.route = self.way_collection.get_route(self.location_rad, self.bearing_rad, self.location_stdev)
_debug(f'Mapd *****: Failed to update location in route. Regenerated with route: \n{self.route}\n********')
# We use the lock when updating the route, as it reads `way_collection` which can ben updated by
# a new query result from the _query_thread.
_debug('Mapd: Locking to update route.', log_to_cloud=False)
with self._lock:
update_proc()
_debug('Mapd: Releasing Lock to update route', log_to_cloud=False)
def publish(self, pm, sm):
# Ensure we have a route currently located
if self.route is None or not self.route.located:
_debug('Mapd: Skipping liveMapDataSP message as there is no route or is not located.')
return
# Ensure we have a route update since last publish
if self.last_publish_fix_timestamp == self.last_route_update_fix_timestamp:
_debug('Mapd: Skipping liveMapDataSP since there is no new gps fix.')
return
self.last_publish_fix_timestamp = self.last_route_update_fix_timestamp
speed_limit = self.route.current_speed_limit
next_speed_limit_section = self.route.next_speed_limit_section
turn_speed_limit_section = self.route.current_curvature_speed_limit_section
horizon_mts = self.gps_speed * LOOK_AHEAD_HORIZON_TIME
next_turn_speed_limit_sections = self.route.next_curvature_speed_limit_sections(horizon_mts)
current_road_name = self.route.current_road_name
map_data_msg = messaging.new_message('liveMapDataSP')
map_data_msg.valid = sm.all_alive(service_list=[self.gps_sock]) and \
sm.all_valid(service_list=[self.gps_sock])
liveMapDataSP = map_data_msg.liveMapDataSP
liveMapDataSP.lastGpsTimestamp = self.last_gps.unixTimestampMillis
liveMapDataSP.lastGpsLatitude = float(self.last_gps.latitude)
liveMapDataSP.lastGpsLongitude = float(self.last_gps.longitude)
liveMapDataSP.lastGpsSpeed = float(self.last_gps.speed)
liveMapDataSP.lastGpsBearingDeg = float(self.last_gps.bearingDeg)
liveMapDataSP.lastGpsAccuracy = float(self.last_gps.accuracy if self.gps_sock == 'gpsLocationExternal' else 1) # gpsLocation doesnt report accuracy
liveMapDataSP.lastGpsBearingAccuracyDeg = float(self.last_gps.bearingAccuracyDeg)
liveMapDataSP.speedLimitValid = bool(speed_limit is not None)
liveMapDataSP.speedLimit = float(speed_limit if speed_limit is not None else 0.0)
liveMapDataSP.speedLimitAheadValid = bool(next_speed_limit_section is not None)
liveMapDataSP.speedLimitAhead = float(next_speed_limit_section.value
if next_speed_limit_section is not None else 0.0)
liveMapDataSP.speedLimitAheadDistance = float(next_speed_limit_section.start
if next_speed_limit_section is not None else 0.0)
liveMapDataSP.turnSpeedLimitValid = bool(turn_speed_limit_section is not None)
liveMapDataSP.turnSpeedLimit = float(turn_speed_limit_section.value
if turn_speed_limit_section is not None else 0.0)
liveMapDataSP.turnSpeedLimitSign = int(turn_speed_limit_section.curv_sign
if turn_speed_limit_section is not None else 0)
liveMapDataSP.turnSpeedLimitEndDistance = float(turn_speed_limit_section.end
if turn_speed_limit_section is not None else 0.0)
liveMapDataSP.turnSpeedLimitsAhead = [float(s.value) for s in next_turn_speed_limit_sections]
liveMapDataSP.turnSpeedLimitsAheadDistances = [float(s.start) for s in next_turn_speed_limit_sections]
liveMapDataSP.turnSpeedLimitsAheadSigns = [float(s.curv_sign) for s in next_turn_speed_limit_sections]
liveMapDataSP.currentRoadName = str(current_road_name if current_road_name is not None else "")
liveMapDataSP.dataType = self.data_type
pm.send('liveMapDataSP', map_data_msg)
_debug(f'Mapd *****: Publish: \n{map_data_msg}\n********', log_to_cloud=False)
# provides live map data information
def mapd_thread(sm=None, pm=None):
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("mapd: failed to set core affinity")
mapd = MapD()
rk = Ratekeeper(1., print_delay_threshold=None) # Keeps rate at 1 hz
# *** setup messaging
if sm is None:
sm = messaging.SubMaster(['gpsLocation', 'gpsLocationExternal', 'carControl'])
if pm is None:
pm = messaging.PubMaster(['liveMapDataSP'])
while True:
sm.update()
mapd.udpate_state(sm)
mapd.update_gps(sm)
mapd.updated_osm_data()
mapd.update_route()
mapd.publish(pm, sm)
rk.keep_time()
def main(sm=None, pm=None):
mapd_thread(sm, pm)
if __name__ == "__main__":
main()
-92
View File
@@ -1,92 +0,0 @@
{
"== None ==": {
"name": "",
"waypoint": "",
"url": ""
},
"Australia": {
"name": "australia",
"waypoint": "514911884",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EathcAhPP8dHiTdBiFPIGYoB7zwlUR79OkSTH5dnS-9Shg?e=9cS6er&download=1"
},
"Brazil": {
"name": "brazil",
"waypoint": "124199196",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EXWcrd8ukahDtoFDXVOxvkYBjGGa9b29fVvRdJlADv3YdA?e=jwabo2&download=1"
},
"Canada": {
"name": "canada",
"waypoint": "68588664",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETpwTMZftJpCkep2goPmzUQBl5SW_YfHBKLTd7w5CUagkA?e=dshC10&download=1"
},
"GCC States": {
"name": "gcc-states",
"waypoint": "69021390",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EWtFOVR9mj5CoRpATYR0_tUBWNEcy5jeasp8zO0ZYdduRg?e=Cg0l2Z&download=1"
},
"Germany": {
"name": "germany",
"waypoint": "461526153",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EbNzQuA8jHBCuGch1WY4cuoBJfK1S6m5odj5qMnOu3HA3Q?e=cNDn30&download=1"
},
"Malaysia, Singapore, Brunei": {
"name": "malaysia-singapore-brunei",
"waypoint": "1112741782",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQb9bq_YkE1NuFx0jp-52zoBnAbdo4nyLXMnEY1pUbCB8g?e=CoFiUJ&download=1"
},
"New Zealand": {
"name": "new-zealand",
"waypoint": "154430132",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EVI6eMuNa4dJpXg-r92Vw1EBq5RxR83_7Z5C56AYftg8xQ?e=xouRBl&download=1"
},
"South Africa": {
"name": "south-africa",
"waypoint": "2729449",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQzbZeus-yBNsgP1KQyLn9wBYZVMMknVoHdB7_ewulLbdA?e=DrnKkb&download=1"
},
"Spain": {
"name": "spain",
"waypoint": "4263034",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/Ee21EfLqZxxLjksDCB_2dscBXKN9Kch8ZOYz3cIWnVpBTg?e=LWBJ1n&download=1"
},
"Taiwan": {
"name": "taiwan",
"waypoint": "198637969",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EXgP3Z5_lsBPiQe-aXOOSY4Bu-r54Z_3kKKmT8-IqLO42A?e=tOhUYw&download=1"
},
"Turkey": {
"name": "turkey",
"waypoint": "698359658",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EWJRrYpV7I1IvOW0LUWrDsUBjEjuKRTla0hA4yRbsVX2xw?e=RGcu8n&download=1"
},
"US - Florida": {
"name": "florida",
"waypoint": "147221754",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ET9rAvCaretAnO4TjocAddkBFxAziblxblAn9YWzGk5hTw?e=tts5Oz&download=1"
},
"US - Midwest": {
"name": "us-midwest",
"waypoint": "1059596607",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EaDYnugxokRHhOukMndJuZMBKz-kzPfM6C_-iQcki-r94Q?e=oUX6hz&download=1"
},
"US - Northeast": {
"name": "us-northeast",
"waypoint": "575213527",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ERrlIKNuT4ZIuUOfr4a3BwABLfd7X1wSq9nUrk9buKrIgA?e=HHIT8R&download=1"
},
"US - Pacific": {
"name": "us-pacific",
"waypoint": "112909709",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQOzoMksk9tAluahQ2qm3rQBHOZD03qFU8Aw4o-TFUTuJA?e=CcBOxL&download=1"
},
"US - South": {
"name": "us-south",
"waypoint": "243729876",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETizacxh8lFNk8NMVRyKCEsBMMYZl1iq0r-D4IWoOAd_DQ?e=EVIElm&download=1"
},
"US - West": {
"name": "us-west",
"waypoint": "30023440",
"url": "https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETTRiSKwlEVKoRHtylruy7wBKgITKXdaCUAkJMuGAN2_mA?e=7jlglL&download=1"
}
}
-266
View File
@@ -1,266 +0,0 @@
from selfdrive.mapd.lib.WayCollection import WayCollection
from selfdrive.mapd.lib.geo import vectors, R
from selfdrive.mapd.lib.NodesData import _MIN_NODE_DISTANCE, _ADDED_NODES_DIST, _SPLINE_EVAL_STEP, \
_MIN_SPEED_SECTION_LENGTH, nodes_raw_data_array_for_wr, node_calculations, is_wr_a_valid_divertion_from_node, \
spline_curvature_calculations, speed_limits_for_curvatures_data
from scipy.interpolate import splev, splprep
import numpy as np
import overpy
class MockNodesData():
def __init__(self, way_coords):
self.degrees = np.array(way_coords)
self.radians = np.radians(self.degrees)
# *****************
# Expected code implementation nodes_data
self.v = vectors(self.radians) * R
self.d = np.linalg.norm(self.v, axis=1)
self.b = np.arctan2(self.v[:, 0], self.v[:, 1])
self.v = np.concatenate(([[0., 0.]], self.v))
self.dp = np.concatenate(([0.], self.d))
self.dn = np.concatenate((self.d, [0.]))
self.dr = np.cumsum(self.dp, axis=0)
self.b = np.concatenate((self.b, [self.b[-1]]))
# Expected code implementation spline_curvature_calculations
vect = self.v
dist_prev = self.dp
too_far_idxs = np.nonzero(self.dp >= _MIN_NODE_DISTANCE)[0]
for idx in too_far_idxs[::-1]:
dp = dist_prev[idx] # distance of vector that needs to be replaced by higher resolution vectors.
n = int(np.ceil(dp / _ADDED_NODES_DIST)) # number of vectors that need to be added.
new_v = vect[idx, :] / n # new relative vector to insert.
vect = np.delete(vect, idx, axis=0) # remove the relative vector to be replaced by the insertion of new vectors.
vect = np.insert(vect, [idx] * n, [new_v] * n, axis=0) # insert n new relative vectors
ds = np.cumsum(dist_prev, axis=0)
vs = np.cumsum(vect, axis=0)
tck, u = splprep([vs[:, 0], vs[:, 1]]) # pylint: disable=W0632
n = max(int(ds[-1] / _SPLINE_EVAL_STEP), len(u))
unew = np.arange(0, n + 1) / n
d1 = splev(unew, tck, der=1)
d2 = splev(unew, tck, der=2)
num = d1[0] * d2[1] - d1[1] * d2[0]
den = (d1[0]**2 + d1[1]**2)**(1.5)
self.curv = num / den
self.curv_ds = unew * ds[-1]
# *****************
class MockCurveSection():
def __init__(self, func, di=0., df=1000., step=10.):
self.di = di
self.df = df
self.n = (df - di) // step
self.u = np.arange(0, self.n + 1) / self.n
self.curv_ds = self.u * (df - di) + di
self.curv = func(self.u)
self.curv_abs = np.abs(self.curv)
self.curv_sec = np.column_stack((self.curv_abs, np.sign(self.curv), self.curv_ds))
class MockOSMQueryResponse():
def __init__(self, xml_path, query_center):
self.api = overpy.Overpass()
self.query_center = np.radians(np.array(query_center))
with open(xml_path, 'r') as f:
overpass_xml = f.read()
self.ways = self.api.parse_xml(overpass_xml).ways
self.wayCollection = WayCollection(self.ways, self.query_center)
class MockRouteData():
def __init__(self, way_ids, way_collection, first_node_id): # way)ids must be in order forming a route.
self.wrs = [next(wr for wr in way_collection.way_relations if wr.id == way_id) for way_id in way_ids]
self.way_collection = way_collection
self.first_node_id = first_node_id
def reset(self):
way_relations = self.wrs
wr_index = self.way_collection.wr_index
# Nodes Data processing expects way relations to be updated with direction before running.
for idx, wr in enumerate(way_relations):
if idx == 0:
wr.update_direction_from_starting_node(self.first_node_id)
else:
wr.update_direction_from_starting_node(way_relations[idx - 1].last_node.id)
# ***** Expected calculations
self._nodes_data = np.array([])
self._divertions = [[]]
self._curvature_speed_sections_data = np.array([])
way_count = len(way_relations)
if way_count == 0:
return
# We want all the nodes from the last way section
nodes_data = nodes_raw_data_array_for_wr(way_relations[-1])
# For the ways before the last in the route we want all the nodes but the last, as that one is the first on
# the next section. Collect them, append last way node data and concatenate the numpy arrays.
if way_count > 1:
wrs_data = tuple([nodes_raw_data_array_for_wr(wr, drop_last=True) for wr in way_relations[:-1]])
wrs_data += (nodes_data,)
nodes_data = np.concatenate(wrs_data)
# Get a subarray with lat, lon to compute the remaining node values.
lat_lon_array = nodes_data[:, [1, 2]]
points = np.radians(lat_lon_array)
# Ensure we have more than 3 points, if not calculations are not possible.
if len(points) <= 3:
return
vect, dist_prev, dist_next, dist_route, bearing = node_calculations(points)
# append calculations to nodes_data
# nodes_data structure: [id, lat, lon, speed_limit, x, y, dist_prev, dist_next, dist_route, bearing]
self._nodes_data = np.column_stack((nodes_data, vect, dist_prev, dist_next, dist_route, bearing))
# Build route diversion options data from the wr_index.
wr_ids = [wr.id for wr in way_relations]
self._divertions = [[wr for wr in wr_index.way_relations_with_edge_node_id(node_id)
if is_wr_a_valid_divertion_from_node(wr, node_id, wr_ids)]
for node_id in nodes_data[:, 0]]
# Store calculcations for curvature sections speed limits. We need more than 3 points to be able to process.
# _curvature_speed_sections_data structure: [dist_start, dist_stop, speed_limits, curv_sign]
if len(vect) > 3:
self._curv, self._curv_ds = spline_curvature_calculations(vect, dist_prev)
self._curvature_speed_sections_data = speed_limits_for_curvatures_data(self._curv, self._curv_ds)
# *****
# Test data in degrees from this road:
# https://www.google.de/maps/@52.209263,13.8723137,13z
_WAY_NODES_COORDS_01 = [
[52.1933703, 13.8723799],
[52.1939477, 13.8711273],
[52.1942004, 13.8705818],
[52.1945408, 13.8698496],
[52.1948447, 13.8691873],
[52.1950772, 13.8685726],
[52.1951168, 13.8684641],
[52.1956681, 13.8670323],
[52.1958716, 13.8664936],
[52.1964366, 13.8649875],
[52.1969283, 13.8636040],
[52.1970203, 13.8634430],
[52.1975486, 13.8626307],
[52.1976354, 13.8624971],
[52.1977827, 13.8621795],
[52.1978564, 13.8619220],
[52.1981843, 13.8604497],
[52.1982614, 13.8602140],
[52.1983351, 13.8600595],
[52.1992768, 13.8579824],
[52.1995107, 13.8574321],
[52.1995948, 13.8572604],
[52.1996818, 13.8571155],
[52.1998000, 13.8570029],
[52.2000659, 13.8568236],
[52.2003868, 13.8566005],
[52.2007182, 13.8564460],
[52.2008760, 13.8564117],
[52.2009865, 13.8564117],
[52.2011390, 13.8564202],
[52.2012267, 13.8564496],
[52.2012544, 13.8564577],
[52.2013179, 13.8564803],
[52.2020491, 13.8571756],
[52.2026014, 13.8576991],
[52.2027592, 13.8578879],
[52.2027960, 13.8579309],
[52.2028960, 13.8580939],
[52.2030170, 13.8583343],
[52.2036587, 13.8597076],
[52.2052946, 13.8633039],
[52.2064332, 13.8658435],
[52.2067856, 13.8666332],
[52.2068961, 13.8668477],
[52.2070777, 13.8670890],
[52.2073723, 13.8674409],
[52.2077457, 13.8679387],
[52.2083874, 13.8687455],
[52.2093341, 13.8699214],
[52.2099652, 13.8707540],
[52.2102282, 13.8712089],
[52.2104228, 13.8715694],
[52.2106122, 13.8718955],
[52.2107619, 13.8721756],
[52.2108695, 13.8723771],
[52.2110747, 13.8727610],
[52.2111514, 13.8729047],
[52.2114010, 13.8733718],
[52.2114694, 13.8735006],
[52.2115430, 13.8736636],
[52.2116086, 13.8737571],
[52.2116770, 13.8738172],
[52.2117611, 13.8738515],
[52.2118664, 13.8738566],
[52.2119322, 13.8738439],
[52.2121058, 13.8737924],
[52.2122583, 13.8737495],
[52.2123265, 13.8737260],
[52.2124213, 13.8736894],
[52.2127466, 13.8734888],
[52.2128263, 13.8734491],
[52.2131313, 13.8733117],
[52.2133943, 13.8731830],
[52.2136625, 13.8731057],
[52.2139465, 13.8730456],
[52.2143619, 13.8730113],
[52.2148773, 13.8729942],
[52.2152275, 13.8730325],
[52.2153110, 13.8730398],
[52.2157442, 13.8730848],
[52.2158833, 13.8731036]]
mockNodesData01 = MockNodesData(_WAY_NODES_COORDS_01)
# OSM Query around B96 south of Berlin
mockOSMResponse01 = MockOSMQueryResponse('selfdrive/mapd/test/mock_osm_response_01.xml',
[52.31400353586984, 13.447158941786366])
# OSM Query on curvy town area south of Germany.
mockOSMResponse02 = MockOSMQueryResponse('selfdrive/mapd/test/mock_osm_response_02.xml',
[48.16573269276522, 9.81418473659117])
mockWayCollection01 = WayCollection(mockOSMResponse01.ways, mockOSMResponse01.query_center)
mockWayCollection02 = WayCollection(mockOSMResponse02.ways, mockOSMResponse02.query_center)
# Normal curvy Way. way id: 179532213 with 35 Nodes.
mockOSMWay_01_01_LongCurvy = next(way for way in mockOSMResponse01.ways if way.id == 179532213)
# Looped way. way id: 29233907
mockOSMWay_01_02_Loop = next(way for way in mockOSMResponse01.ways if way.id == 29233907)
# Complex curvy road through town with intersections. way id:178450395
mockOSMWay_02_01_CurvyTownWithIntersections = next(way for way in mockOSMResponse02.ways if way.id == 178450395)
# Valid diversion for way 02_01 at node: 34785115. way id: 27955186
mockOSMWay_02_02_Divertion_34785115 = next(way for way in mockOSMResponse02.ways if way.id == 27955186)
# 3 node way. way id: 807781992
mockOSMWay_02_03_Short_3_node_way = next(way for way in mockOSMResponse02.ways if way.id == 807781992)
# data composing route 01 in way collection 02
mockRouteData_02_01 = MockRouteData([60890967, 737120246, 601406617, 60890971, 178450395], mockWayCollection02,
first_node_id=201962346)
# data composing route 02 in way collection 02. Single WR
mockRouteData_02_02_single_wr = MockRouteData([178450395], mockWayCollection02, first_node_id=762086638)
# data composing route 03 in way collection 02. Multiple speed limits
mockRouteData_02_03 = MockRouteData([158799549, 798805532, 28707704, 158797898, 602249535, 602249536, 825823509,
178449088, 916462523, 158796386], mockWayCollection02,
first_node_id=252601829)
# 1000mt section with one full sin cycle as curv values.
mockCurveSectionSin = MockCurveSection(lambda x: np.sin(x * 2 * np.pi))
# 200mt section with changing curvature rate.
mockCurveSteepCurvChange = MockCurveSection(lambda x: 0.05 * x**3 - 0.007 * x**2 + 0.001 * x, df=200)
# _MIN_SPEED_SECTION_LENGTH section with changing curvature rate.
mockCurveSteepCurvChangeShort = MockCurveSection(
lambda x: 0.05 * x**3 - 0.007 * x**2 + 0.001 * x, df=_MIN_SPEED_SECTION_LENGTH)
# 200mt section with smooth changing curvature rate. no deviation over 2.
mockCurveSmoothCurveChange = MockCurveSection(lambda x: 0.0002 * x**3 - 0.001 * x**2 + 0.6 * x, df=200)
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-354
View File
@@ -1,354 +0,0 @@
import unittest
import numpy as np
from selfdrive.mapd.lib.geo import DIRECTION
from common.conversions import Conversions as CV
from selfdrive.mapd.lib.WayRelation import WayRelation
from selfdrive.mapd.lib.NodesData import nodes_raw_data_array_for_wr, node_calculations, \
spline_curvature_calculations, split_speed_section_by_sign, split_speed_section_by_curv_degree, speed_section, \
speed_limits_for_curvatures_data, is_wr_a_valid_divertion_from_node, SpeedLimitSection, TurnSpeedLimitSection, \
NodesData, NodeDataIdx
from selfdrive.mapd.test.mock_data import mockOSMWay_01_01_LongCurvy, mockNodesData01, mockCurveSectionSin, \
mockCurveSteepCurvChange, mockCurveSteepCurvChangeShort, mockCurveSmoothCurveChange, \
mockOSMWay_02_01_CurvyTownWithIntersections, mockOSMWay_02_02_Divertion_34785115, mockOSMWay_02_03_Short_3_node_way, \
mockRouteData_02_01, mockRouteData_02_02_single_wr, mockRouteData_02_03
from numpy.testing import assert_array_almost_equal
class TestNodesDataFileFunctions(unittest.TestCase):
def test_nodes_raw_data_array_for_wr(self):
wr = WayRelation(mockOSMWay_01_01_LongCurvy)
data_e = np.array([(n.id, n.lat, n.lon, wr.speed_limit) for n in wr.way.nodes], dtype=float)
data = nodes_raw_data_array_for_wr(wr)
assert_array_almost_equal(data, data_e)
def test_nodes_raw_data_array_for_wr_flips_when_backwards(self):
wr = WayRelation(mockOSMWay_01_01_LongCurvy)
wr.direction = DIRECTION.BACKWARD
data_e = np.array([(n.id, n.lat, n.lon, wr.speed_limit) for n in wr.way.nodes], dtype=float)
data_e = np.flip(data_e, axis=0)
data = nodes_raw_data_array_for_wr(wr)
assert_array_almost_equal(data, data_e)
def test_nodes_raw_data_array_for_wr_drops_last(self):
wr = WayRelation(mockOSMWay_01_01_LongCurvy)
data_e = np.array([(n.id, n.lat, n.lon, wr.speed_limit) for n in wr.way.nodes], dtype=float)[:-1]
data = nodes_raw_data_array_for_wr(wr, drop_last=True)
assert_array_almost_equal(data, data_e)
def test_node_calculations(self):
points = mockNodesData01.radians
v, dp, dn, dr, b = node_calculations(points)
assert_array_almost_equal(v, mockNodesData01.v)
assert_array_almost_equal(dp, mockNodesData01.dp)
assert_array_almost_equal(dn, mockNodesData01.dn)
assert_array_almost_equal(dr, mockNodesData01.dr)
assert_array_almost_equal(b, mockNodesData01.b)
def test_node_calculations_index_error(self):
points = mockNodesData01.radians[:2]
with self.assertRaises(IndexError):
node_calculations(points)
def test_spline_curvature_calculations(self):
vect = mockNodesData01.v
dist_prev = mockNodesData01.dp
curv, curv_ds = spline_curvature_calculations(vect, dist_prev)
assert_array_almost_equal(curv, mockNodesData01.curv)
assert_array_almost_equal(curv_ds, mockNodesData01.curv_ds)
def test_spline_curvature_calculations_with_route_data(self):
mockRouteData_02_01.reset()
nodes_data = mockRouteData_02_01._nodes_data
vect = np.column_stack((nodes_data[:, 4], nodes_data[:, 5]))
dist_prev = nodes_data[:, 6]
curv, curv_ds = spline_curvature_calculations(vect, dist_prev)
assert_array_almost_equal(curv, mockRouteData_02_01._curv)
assert_array_almost_equal(curv_ds, mockRouteData_02_01._curv_ds)
def test_split_speed_section_by_sign(self):
curv_sec = mockCurveSectionSin.curv_sec
new_secs = split_speed_section_by_sign(curv_sec)
# 3 sections with matching initial and final distance
self.assertEqual(len(new_secs), 3)
self.assertEqual(new_secs[0][0][2], mockCurveSectionSin.di)
self.assertEqual(new_secs[2][-1][2], mockCurveSectionSin.df)
# All new sections has same sign internally
for sec in new_secs:
self.assertEqual(np.average(sec, axis=0)[1], sec[0][1])
# Sections change sign
for idx in range(2):
self.assertNotEqual(new_secs[idx][0][1], new_secs[idx + 1][0][1])
# total items consistency
lengths = [len(sec) for sec in new_secs]
self.assertEqual(len(curv_sec), sum(lengths))
def test_split_speed_section_by_curv_degree(self):
curv_sec = mockCurveSteepCurvChange.curv_sec
new_secs = split_speed_section_by_curv_degree(curv_sec)
# 3 sections with matching initial and final distance
self.assertEqual(len(new_secs), 3)
self.assertEqual(new_secs[0][0][2], mockCurveSteepCurvChange.di)
self.assertEqual(new_secs[2][-1][2], mockCurveSteepCurvChange.df)
# Sections split at the right points
split_dist = [sec[-1][2] for sec in new_secs]
self.assertListEqual(split_dist, [50., 150., 200.])
def test_split_speed_section_by_curv_degree_does_nothing_if_short(self):
curv_sec = mockCurveSteepCurvChangeShort.curv_sec
new_secs = split_speed_section_by_curv_degree(curv_sec)
self.assertEqual(len(new_secs), 1)
assert_array_almost_equal(curv_sec, new_secs[0])
def test_split_speed_section_by_curv_degree_does_nothing_if_no_substantial_change(self):
curv_sec = mockCurveSmoothCurveChange.curv_sec
new_secs = split_speed_section_by_curv_degree(curv_sec)
self.assertEqual(len(new_secs), 1)
assert_array_almost_equal(curv_sec, new_secs[0])
def test_speed_section(self):
curv_sec = mockCurveSectionSin.curv_sec
speed_secs = speed_section(curv_sec)
expected = np.array([0., 1000., 1.51657509, 1.])
assert_array_almost_equal(speed_secs, expected)
def test_speed_limits_for_curvatures_data(self):
curv = mockCurveSectionSin.curv
curv_ds = mockCurveSectionSin.curv_ds
expected = np.array([
[10., 490., 1.51657509, 1.],
[510., 990., 1.51657509, -1.]])
limits = speed_limits_for_curvatures_data(curv, curv_ds)
assert_array_almost_equal(limits, expected)
def test_is_wr_a_valid_divertion_from_node(self):
wr = WayRelation(mockOSMWay_02_01_CurvyTownWithIntersections)
mockOSMWay_02_02_Divertion_34785115.tags['oneway'] = 'yes'
wr_div = WayRelation(mockOSMWay_02_02_Divertion_34785115)
# False if id already in route
wr_ids = [wr.id, wr_div.id]
self.assertFalse(is_wr_a_valid_divertion_from_node(wr_div, 34785115, wr_ids))
# True if id not in route, node_id is edge and not prohibited
wr_ids = [wr.id, 11111, 22222]
self.assertTrue(is_wr_a_valid_divertion_from_node(wr_div, 34785115, wr_ids))
# False if id not in route, node_id is edge but prohibited (wrong direction from node 319503453)
self.assertFalse(is_wr_a_valid_divertion_from_node(wr_div, 319503453, wr_ids))
# False if id not in route, node_id is not edge
self.assertFalse(is_wr_a_valid_divertion_from_node(wr_div, 44444, wr_ids))
class TestSpeedLimitSection(unittest.TestCase):
def test_speed_limit_section_init(self):
section = SpeedLimitSection(10., 20., 50.)
self.assertEqual(section.start, 10.)
self.assertEqual(section.end, 20.)
self.assertEqual(section.value, 50.)
class TestTurnSpeedLimitSection(unittest.TestCase):
def test_turn_speed_limit_section_init(self):
section = TurnSpeedLimitSection(10., 20., 50., -1.)
self.assertEqual(section.start, 10.)
self.assertEqual(section.end, 20.)
self.assertEqual(section.value, 50.)
self.assertEqual(section.curv_sign, -1.)
class TestNodesData(unittest.TestCase):
def test_init_with_empty_list(self):
nodesData = NodesData([], {})
self.assertEqual(len(nodesData._nodes_data), 0)
num_diverstions = sum([len(d) for d in nodesData._divertions])
self.assertEqual(num_diverstions, 0)
self.assertEqual(len(nodesData._curvature_speed_sections_data), 0)
def test_init_with_single_wr_includes_all_wr_nodes(self):
mockRouteData_02_02_single_wr.reset()
way_relations = mockRouteData_02_02_single_wr.wrs
wr_index = mockRouteData_02_02_single_wr.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
assert_array_almost_equal(nodesData._nodes_data, mockRouteData_02_02_single_wr._nodes_data)
assert_array_almost_equal(nodesData._curvature_speed_sections_data,
mockRouteData_02_02_single_wr._curvature_speed_sections_data)
self.assertListEqual(nodesData._divertions, mockRouteData_02_02_single_wr._divertions)
self.assertEqual(len(nodesData._nodes_data), len(way_relations[0].way.nodes))
self.assertEqual(len(nodesData._curvature_speed_sections_data), 6)
num_diverstions = sum([len(d) for d in nodesData._divertions])
self.assertEqual(num_diverstions, 6)
def test_init_with_less_than_4_nodes(self):
wr_t = WayRelation(mockOSMWay_02_03_Short_3_node_way)
nodesData = NodesData([wr_t], {})
self.assertEqual(len(nodesData._nodes_data), 0)
num_diverstions = sum([len(d) for d in nodesData._divertions])
self.assertEqual(num_diverstions, 0)
self.assertEqual(len(nodesData._curvature_speed_sections_data), 0)
def test_init_with_multiple_wr(self):
mockRouteData_02_01.reset()
way_relations = mockRouteData_02_01.wrs
wr_index = mockRouteData_02_01.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
assert_array_almost_equal(nodesData._nodes_data, mockRouteData_02_01._nodes_data)
assert_array_almost_equal(nodesData._curvature_speed_sections_data, mockRouteData_02_01._curvature_speed_sections_data)
self.assertListEqual(nodesData._divertions, mockRouteData_02_01._divertions)
self.assertEqual(len(nodesData._curvature_speed_sections_data), 9)
num_diverstions = sum([len(d) for d in nodesData._divertions])
self.assertEqual(num_diverstions, 14)
def test_count(self):
mockRouteData_02_01.reset()
way_relations = mockRouteData_02_01.wrs
wr_index = mockRouteData_02_01.way_collection.wr_index
num_n = sum([len(wr.way.nodes) for wr in way_relations]) - len(way_relations) + 1
nodesData = NodesData(way_relations, wr_index)
self.assertEqual(nodesData.count, num_n)
def test_get_on_empty(self):
wr_t = WayRelation(mockOSMWay_02_03_Short_3_node_way)
nodesData = NodesData([wr_t], {})
assert_array_almost_equal(nodesData.get(NodeDataIdx.node_id), np.array([]))
def test_get_values(self):
mockRouteData_02_01.reset()
way_relations = mockRouteData_02_01.wrs
wr_index = mockRouteData_02_01.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
assert_array_almost_equal(nodesData.get(NodeDataIdx.node_id), mockRouteData_02_01._nodes_data[:, 0])
assert_array_almost_equal(nodesData.get(NodeDataIdx.lat), mockRouteData_02_01._nodes_data[:, 1])
assert_array_almost_equal(nodesData.get(NodeDataIdx.lon), mockRouteData_02_01._nodes_data[:, 2])
assert_array_almost_equal(nodesData.get(NodeDataIdx.speed_limit), mockRouteData_02_01._nodes_data[:, 3])
assert_array_almost_equal(nodesData.get(NodeDataIdx.x), mockRouteData_02_01._nodes_data[:, 4])
assert_array_almost_equal(nodesData.get(NodeDataIdx.y), mockRouteData_02_01._nodes_data[:, 5])
assert_array_almost_equal(nodesData.get(NodeDataIdx.dist_prev), mockRouteData_02_01._nodes_data[:, 6])
assert_array_almost_equal(nodesData.get(NodeDataIdx.dist_next), mockRouteData_02_01._nodes_data[:, 7])
assert_array_almost_equal(nodesData.get(NodeDataIdx.dist_route), mockRouteData_02_01._nodes_data[:, 8])
assert_array_almost_equal(nodesData.get(NodeDataIdx.bearing), mockRouteData_02_01._nodes_data[:, 9])
def test_speed_limits_ahead_from_empty(self):
wr_t = WayRelation(mockOSMWay_02_03_Short_3_node_way)
nodesData = NodesData([wr_t], {})
self.assertEqual(len(nodesData.speed_limits_ahead(1, 10.)), 0)
def test_speed_limits_ahead(self):
mockRouteData_02_03.reset()
way_relations = mockRouteData_02_03.wrs
wr_index = mockRouteData_02_03.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
# empty when ahead_idx is none.
self.assertEqual(len(nodesData.speed_limits_ahead(None, 10.)), 0)
# All limist from 0
all_limits = nodesData.speed_limits_ahead(1, nodesData.get(NodeDataIdx.dist_next)[0])
self.assertEqual(len(all_limits), 4) # 4 limits on this mock road.
self.assertListEqual([sl.value for sl in all_limits], [v * CV.KPH_TO_MS for v in [50, 100, 50, 100]])
for idx, sl in enumerate(all_limits):
self.assertTrue(sl.end > sl.start)
self.assertTrue(sl.value > 0.)
if idx == 0:
self.assertEqual(sl.start, 0.)
else:
self.assertEqual(sl.start, all_limits[idx - 1].end)
self.assertNotEqual(sl.value, all_limits[idx - 1].value)
def test_distance_to_end_from_empty(self):
wr_t = WayRelation(mockOSMWay_02_03_Short_3_node_way)
nodesData = NodesData([wr_t], {})
self.assertIsNone(nodesData.distance_to_end(1, 10.))
def test_distance_to_end(self):
mockRouteData_02_03.reset()
way_relations = mockRouteData_02_03.wrs
wr_index = mockRouteData_02_03.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
# none when ahead_idx is none.
self.assertIsNone(nodesData.distance_to_end(None, 10.))
# From the beginning
expected = np.sum(nodesData.get(NodeDataIdx.dist_next))
self.assertAlmostEqual(nodesData.distance_to_end(1, nodesData.get(NodeDataIdx.dist_next)[0]), expected)
self.assertAlmostEqual(nodesData.get(NodeDataIdx.dist_route)[-1], expected)
# From the node next to last
expected = nodesData.get(NodeDataIdx.dist_next)[-2]
self.assertAlmostEqual(nodesData.distance_to_end(nodesData.count - 2, 0.), expected)
def test_distance_to_node(self):
mockRouteData_02_03.reset()
way_relations = mockRouteData_02_03.wrs
wr_index = mockRouteData_02_03.way_collection.wr_index
nodesData = NodesData(way_relations, wr_index)
dist_to_node_ahead = 10.
node_id = 1887995486 # Some node id in the middle of the way. idx 50
node_idx = np.nonzero(nodesData.get(NodeDataIdx.node_id) == node_id)[0][0]
# none when ahead_idx is none.
self.assertIsNone(nodesData.distance_to_node(node_id, None, dist_to_node_ahead))
# From the beginning
expected = nodesData.get(NodeDataIdx.dist_route)[node_idx]
self.assertAlmostEqual(nodesData.distance_to_node(node_id, 1, nodesData.get(NodeDataIdx.dist_next)[0]), expected)
# From the end
expected = -np.sum(nodesData.get(NodeDataIdx.dist_next)[node_idx:])
self.assertAlmostEqual(nodesData.distance_to_node(node_id, len(nodesData.get(NodeDataIdx.node_id)) - 1, 0.), expected)
# From some node behind including dist to node ahead
ahead_idx = node_idx - 10
expected = np.sum(nodesData.get(NodeDataIdx.dist_next)[ahead_idx:node_idx]) + dist_to_node_ahead
self.assertAlmostEqual(nodesData.distance_to_node(node_id, ahead_idx, dist_to_node_ahead), expected)
# From some node ahead including dist to node ahead
ahead_idx = node_idx + 10
expected = -np.sum(nodesData.get(NodeDataIdx.dist_next)[node_idx:ahead_idx]) + dist_to_node_ahead
self.assertAlmostEqual(nodesData.distance_to_node(node_id, ahead_idx, dist_to_node_ahead), expected)
# TODO: Missing tests for curvatures_speed_limit_sections_ahead and possible_divertions
-651
View File
@@ -1,651 +0,0 @@
import copy
import unittest
import numpy as np
from unittest import mock
from numpy.testing import assert_array_almost_equal
from datetime import datetime as dt, timezone, timedelta
from common.conversions import Conversions as CV
from selfdrive.mapd.lib.WayRelation import WayRelation, is_osm_time_condition_active, \
conditional_speed_limit_for_osm_tag_limit_string, speed_limit_for_osm_tag_limit_string
from selfdrive.mapd.config import LANE_WIDTH
from selfdrive.mapd.lib.geo import DIRECTION, R, vectors
from selfdrive.mapd.test.mock_data import mockOSMWay_01_01_LongCurvy, mockOSMWay_01_02_Loop, \
mockOSMWay_02_01_CurvyTownWithIntersections
class TestWayRelationFileFunctions(unittest.TestCase):
def test_speed_limit_for_osm_tag_limit_string(self):
values = [
None, # Invalid
"1000", # Invalid
"60 kph", # Invalid
"100",
"30 mph",
"DE:zone:40",
"DE:zone:50 mph",
"AR:urban",
"CZ:pedestrian_zone",
"DK:urban",
"DK:rural",
"DK:motorway",
"DE:living_street",
"DE:residential",
"DE:urban",
"DE:rural",
"DE:trunk", # No limit
"DE:motorway", # No limit
"GB:nsl_restricted",
"GB:nsl_single",
"GB:nsl_dual",
"GB:motorway",
"GB:invalid", # Invalid
]
expected = [
0.,
0.,
0.,
100. * CV.KPH_TO_MS,
30. * CV.MPH_TO_MS,
40. * CV.KPH_TO_MS,
50. * CV.MPH_TO_MS,
40. * CV.KPH_TO_MS,
20. * CV.KPH_TO_MS,
50. * CV.KPH_TO_MS,
80. * CV.KPH_TO_MS,
130. * CV.KPH_TO_MS,
7. * CV.KPH_TO_MS,
30. * CV.KPH_TO_MS,
50. * CV.KPH_TO_MS,
100. * CV.KPH_TO_MS,
0.,
0.,
30. * CV.MPH_TO_MS,
60. * CV.MPH_TO_MS,
70. * CV.MPH_TO_MS,
70. * CV.MPH_TO_MS,
0.,
]
result = [speed_limit_for_osm_tag_limit_string(sls) for sls in values]
self.assertEqual(result, expected)
@mock.patch('selfdrive.mapd.lib.WayRelation.dt')
def test_is_osm_time_condition_active(self, mock_dt):
tz = timezone(timedelta(hours=1), 'berlin')
wed_10_10_am = dt(2021, 9, 1, 10, 10, 0)
mock_dt.now.return_value = wed_10_10_am
mock_dt.tzinfo = tz
mock_dt.combine = dt.combine
mock_dt.strptime = dt.strptime
values = [
"WE", # Invalid
"We",
"Mo",
"Fr",
"Tu-Th",
"10:00", # Invalid
"10:00-10:30",
"We 10:00-10:30",
"SU 10:00-10:30", # Valid, SU string not considered a day string.
"Sa 10:00-10:30",
"Tu-Th 10:00-10:30",
]
expected = [
False, # Invalid
True,
False,
False,
True,
False, # Invalid
True,
True,
True,
False,
True,
]
result = [is_osm_time_condition_active(cs) for cs in values]
self.assertEqual(result, expected)
@mock.patch('selfdrive.mapd.lib.WayRelation.dt')
def test_conditional_speed_limit_for_osm_tag_limit_string(self, mock_dt):
tz = timezone(timedelta(hours=1), 'berlin')
wed_10_10_am = dt(2021, 9, 1, 10, 10, 0)
mock_dt.now.return_value = wed_10_10_am
mock_dt.tzinfo = tz
mock_dt.combine = dt.combine
mock_dt.strptime = dt.strptime
values = [
None, # Invalid
"Hola", # Invalid
"100 @ (WE)", # Invalid
"x @ (We)", # Invalid
"100 @ (We)",
"100 @ (Mo)",
"100 @ (Fr)",
"100 @ (Tu-Th)",
"100 @ (10:00)", # Invalid
"100 @ (10:00-10:30)",
"100 @ (We 10:00-10:30)",
"100 @ (SU 10:00-10:30)", # Valid, SU string not considered a day string.
"100 @ (Sa 10:00-10:30)",
"100 @ (Tu-Th 10:00-10:30)",
"100 @ (Mo-Th;Su)",
"100 @ (Mo Th;Fr-Sa)",
"100 @ (Fr-Su;Mo-Tu)",
"100 @ (10:00-10:30;15:00-16:00)",
"100 @ (We;Mo-Tu)",
"100 @ (We 10:00-10:30;Th 15:00-16:00)",
"100 @ (Tu 10:00-10:30;Th 15:00-16:00)",
]
_100 = 100. * CV.KPH_TO_MS
expected = [
0., # Invalid
0., # Invalid
0., # Invalid
0., # Invalid
_100,
0.,
0.,
_100,
0., # Invalid
_100,
_100,
_100,
0.,
_100,
_100,
_100,
0.,
_100,
_100,
_100,
0.
]
result = [conditional_speed_limit_for_osm_tag_limit_string(ls) for ls in values]
self.assertEqual(result, expected)
class TestWayRelation(unittest.TestCase):
def test_way_relation_init(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
nodes_np_expected = np.radians(np.array([[node.lat, node.lon] for node in wayRelation.way.nodes], dtype=float))
v = vectors(wayRelation._nodes_np)
way_distances_expected = np.linalg.norm(v * R, axis=1)
way_bearings_expected = np.arctan2(v[:, 0], v[:, 1])
bbox_expected = np.array([
[0.91321784, 0.2346417],
[0.91344672, 0.23475751]])
self.assertEqual(wayRelation.way.id, 179532213)
self.assertIsNone(wayRelation.parent_wr_id)
self.assertEqual(wayRelation.direction, DIRECTION.NONE)
self.assertEqual(wayRelation._speed_limit, None)
self.assertEqual(wayRelation._one_way, 'yes')
self.assertEqual(wayRelation.name, None)
self.assertEqual(wayRelation.ref, 'B 96')
self.assertEqual(wayRelation.highway_type, 'trunk')
self.assertEqual(wayRelation.highway_rank, 10)
self.assertEqual(wayRelation.lanes, 2)
assert_array_almost_equal(wayRelation._nodes_np, nodes_np_expected)
assert_array_almost_equal(wayRelation._way_distances, way_distances_expected)
assert_array_almost_equal(wayRelation._way_bearings, way_bearings_expected)
assert_array_almost_equal(wayRelation.bbox, bbox_expected)
self.assertEqual(wayRelation.edge_nodes_ids, [wayRelation.way.nodes[0].id, wayRelation.way.nodes[-1].id])
def test_way_relation_init_with_parent(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy, parent=WayRelation(mockOSMWay_01_02_Loop))
self.assertEqual(wayRelation.way.id, 179532213)
self.assertEqual(wayRelation.parent_wr_id, 29233907)
def test_way_relation_equality(self):
wayRelation1 = WayRelation(mockOSMWay_01_01_LongCurvy)
wayRelation2 = copy.copy(wayRelation1)
wayRelation3 = copy.deepcopy(wayRelation1)
wayRelation3.way.id = 123
self.assertEqual(wayRelation1, wayRelation2)
self.assertNotEqual(wayRelation1, wayRelation3)
def test_way_relation_reset_location_variables(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
self.make_wayRelation_location_dirty(wayRelation)
wayRelation.reset_location_variables()
self.assert_wayRelation_variables_reset(wayRelation)
def test_way_relation_id(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
self.assertEqual(wayRelation.id, 179532213)
def test_way_relation_road_name(self):
# road name when no tag for name or ref
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
self.assertIsNone(wayRelation.road_name)
# road name based on ref tag
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
self.assertEqual(wayRelation.road_name, "B 96")
# road name based on name tag
wayRelation = WayRelation(mockOSMWay_02_01_CurvyTownWithIntersections)
self.assertEqual(wayRelation.road_name, "Hauptstraße")
def test_way_relation_update_resets_on_update(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
self.make_wayRelation_location_dirty(wayRelation)
location_rad = np.array([0., 0.]) # Location outside bbox
wayRelation.update(location_rad, 0., 10.)
self.assertFalse(wayRelation.is_location_in_bbox(location_rad))
self.assert_wayRelation_variables_reset(wayRelation)
def test_way_relation_update_only_resets_if_no_possible_found(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
location_rad = wayRelation.bbox[0] # Location inside bbox but outside actual way (due to padding)
wayRelation.update(location_rad, 0., 10.)
self.assertTrue(wayRelation.is_location_in_bbox(location_rad))
self.assert_wayRelation_variables_reset(wayRelation)
def test_way_relation_updates_in_the_correct_direction_with_correct_property_values(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
location_rad = np.radians(np.array([52.32855593146639, 13.445320150125069]))
bearing_rad = 0.
wayRelation.update(location_rad, bearing_rad, 10.)
self.assertTrue(wayRelation.is_location_in_bbox(location_rad))
self.assertEqual(wayRelation.direction, DIRECTION.FORWARD)
self.assertEqual(wayRelation.ahead_idx, 17)
self.assertEqual(wayRelation.behind_idx, 16)
self.assertAlmostEqual(wayRelation._distance_to_way, 3.43290781621360)
self.assertAlmostEqual(wayRelation._active_bearing_delta, 0.320717420388962)
self.assertAlmostEqual(wayRelation.distance_to_node_ahead, 25.4998961709014)
self.assertTrue(wayRelation.active)
self.assertFalse(wayRelation.diverting)
assert_array_almost_equal(wayRelation.location_rad, location_rad)
self.assertEqual(wayRelation.bearing_rad, bearing_rad)
self.assertIsNone(wayRelation._speed_limit)
bearing_rad = 180.
wayRelation.update(location_rad, bearing_rad, 10.)
self.assertTrue(wayRelation.is_location_in_bbox(location_rad))
self.assertEqual(wayRelation.direction, DIRECTION.BACKWARD)
self.assertEqual(wayRelation.ahead_idx, 16)
self.assertEqual(wayRelation.behind_idx, 17)
self.assertAlmostEqual(wayRelation._distance_to_way, 3.43290781621360)
self.assertAlmostEqual(wayRelation._active_bearing_delta, 0.9507682562504284)
self.assertAlmostEqual(wayRelation.distance_to_node_ahead, 11.11623371145368)
self.assertTrue(wayRelation.active)
self.assertFalse(wayRelation.diverting)
assert_array_almost_equal(wayRelation.location_rad, location_rad)
self.assertEqual(wayRelation.bearing_rad, bearing_rad)
self.assertIsNone(wayRelation._speed_limit)
def test_way_relation_updates_with_location_closest_to_way_when_multiple_possible(self):
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
location_rad = np.radians(np.array([52.313303275461564, 13.437729236325788]))
bearing_rad = np.radians(10.)
wayRelation.update(location_rad, bearing_rad, 10.)
self.assertTrue(wayRelation.is_location_in_bbox(location_rad))
self.assertEqual(wayRelation.direction, DIRECTION.BACKWARD)
self.assertEqual(wayRelation.ahead_idx, 26)
self.assertEqual(wayRelation.behind_idx, 27)
self.assertAlmostEqual(wayRelation._distance_to_way, 10.151775235257011)
self.assertAlmostEqual(wayRelation._active_bearing_delta, 0.06371131069242782)
self.assertAlmostEqual(wayRelation.distance_to_node_ahead, 10.174073707120915)
self.assertTrue(wayRelation.active)
self.assertFalse(wayRelation.diverting)
assert_array_almost_equal(wayRelation.location_rad, location_rad)
self.assertEqual(wayRelation.bearing_rad, bearing_rad)
self.assertIsNone(wayRelation._speed_limit)
def test_way_relation_updates_will_become_inactive_if_too_far_from_way(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
# Location is 24.9 mts away from the way. There are 2 Lanes in this way.
location_rad = np.radians(np.array([52.328634560607746, 13.445609877522788]))
location_stdev = 5.5 # threshold is 4 * location_stdev + LANE_WIDTH
distance_threshold = 4. * location_stdev + wayRelation.lanes * LANE_WIDTH / 2.
wayRelation.update(location_rad, 0., location_stdev)
self.assertTrue(wayRelation.active)
self.assertLess(wayRelation._distance_to_way, distance_threshold)
location_stdev = 5.
wayRelation.update(location_rad, 0., location_stdev)
self.assertFalse(wayRelation.active)
def test_way_relation_updates_will_update_diverting_correctly(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
# Location is 24.9 mts away from the way. There are 2 Lanes in this way.
location_rad = np.radians(np.array([52.328634560607746, 13.445609877522788]))
location_stdev = 11.
distance_threshold = 2. * location_stdev + wayRelation.lanes * LANE_WIDTH / 2.
wayRelation.update(location_rad, 0., location_stdev)
self.assertLess(wayRelation._distance_to_way, distance_threshold)
self.assertFalse(wayRelation.diverting)
location_stdev = 10.
distance_threshold = 2. * location_stdev + wayRelation.lanes * LANE_WIDTH / 2.
wayRelation.update(location_rad, 0., location_stdev)
self.assertGreater(wayRelation._distance_to_way, distance_threshold)
self.assertTrue(wayRelation.diverting)
def test_way_relation_update_direction_from_starting_node_resets_speed_limit(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
wayRelation._speed_limit = 10.
wayRelation.update_direction_from_starting_node(wayRelation.way.nodes[0].id)
self.assertIsNone(wayRelation._speed_limit)
def test_way_relation_update_direction_from_starting_node_updates_correctly(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
wayRelation.update_direction_from_starting_node(wayRelation.way.nodes[0].id)
self.assertEqual(wayRelation.direction, DIRECTION.FORWARD)
wayRelation.update_direction_from_starting_node(wayRelation.way.nodes[-1].id)
self.assertEqual(wayRelation.direction, DIRECTION.BACKWARD)
wayRelation.update_direction_from_starting_node(0)
self.assertEqual(wayRelation.direction, DIRECTION.NONE)
def test_way_relation_is_location_in_bbox(self):
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
bbox = wayRelation.bbox
loc_avg = np.average(bbox, axis=0)
loc_min = np.min(bbox, axis=0)
loc_max = np.max(bbox, axis=0)
locations = [
loc_avg,
loc_min,
loc_max,
[loc_avg[0], loc_min[1]],
[loc_avg[0], loc_max[1]],
[loc_min[0], loc_avg[1]],
[loc_max[0], loc_avg[1]],
loc_min - 0.1,
loc_max + 0.1,
[loc_avg[0], loc_min[1] - 0.1],
[loc_avg[0], loc_max[1] + 0.1],
[loc_min[0] - 0.1, loc_avg[1]],
[loc_max[0] + 0.1, loc_avg[1]],
]
is_in = [wayRelation.is_location_in_bbox(loc) for loc in locations]
self.assertEqual(is_in, [True, True, True, True, True, True, True, False, False, False, False, False, False])
def test_way_relation_speed_limit_when_set(self):
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
wayRelation._speed_limit = 10.
self.assertEqual(wayRelation.speed_limit, 10.)
@mock.patch('selfdrive.mapd.lib.WayRelation.dt')
def test_way_relation_speed_limit_conditional(self, mock_dt):
tz = timezone(timedelta(hours=1), 'berlin')
wed_10_10_am = dt(2021, 9, 1, 10, 10, 0)
mock_dt.now.return_value = wed_10_10_am
mock_dt.tzinfo = tz
mock_dt.combine = dt.combine
mock_dt.strptime = dt.strptime
# Reset all tags before teting
mockOSMWay_01_02_Loop.tags = {}
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
# No Value
self.assertEqual(wayRelation.speed_limit, 0.)
# Value on both directions
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed:conditional"] = "100 @ (We 10:00-10:30)"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
# Value on forward
wayRelation.way.tags.pop("maxspeed:conditional")
wayRelation._speed_limit = None
wayRelation.direction = DIRECTION.FORWARD
self.assertEqual(wayRelation.speed_limit, 0.)
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed:forward:conditional"] = "100 @ (We 10:00-10:30)"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
# Value on backward
wayRelation._speed_limit = None
wayRelation.direction = DIRECTION.BACKWARD
self.assertEqual(wayRelation.speed_limit, 0.)
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed:backward:conditional"] = "100 @ (We 10:00-10:30)"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
def test_way_relation_speed_limit_maxspeed(self):
# Reset all tags before teting
mockOSMWay_01_02_Loop.tags = {}
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
# No Value
self.assertEqual(wayRelation.speed_limit, 0.)
# Value on both directions
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed"] = "100"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
# Value on forward
wayRelation.way.tags.pop("maxspeed")
wayRelation._speed_limit = None
wayRelation.direction = DIRECTION.FORWARD
self.assertEqual(wayRelation.speed_limit, 0.)
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed:forward"] = "100"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
# Value on backward
wayRelation._speed_limit = None
wayRelation.direction = DIRECTION.BACKWARD
self.assertEqual(wayRelation.speed_limit, 0.)
wayRelation._speed_limit = None
wayRelation.way.tags["maxspeed:backward"] = "100"
self.assertEqual(wayRelation.speed_limit, 100. * CV.KPH_TO_MS)
def test_way_relation_active_bearing_delta_reflects_internal_value(self):
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
wayRelation._active_bearing_delta = 10.
self.assertEqual(wayRelation.active_bearing_delta, 10.)
def test_way_relation_is_one_way(self):
# Setup initial tags
mockOSMWay_01_02_Loop.tags = {
'oneway': 'yes',
'highway': 'unclassified'
}
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
# oneway = yes
self.assertTrue(wayRelation.is_one_way)
# oneway non existing
wayRelation._one_way = None
self.assertFalse(wayRelation.is_one_way)
# highway = motorway
wayRelation.highway_type = 'motorway'
self.assertTrue(wayRelation.is_one_way)
def test_way_relation_is_prohibited(self):
# Setup initial tags
mockOSMWay_01_02_Loop.tags = {
'oneway': 'yes'
}
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
# Direction undefined
wayRelation.direction = DIRECTION.NONE
self.assertTrue(wayRelation.is_prohibited)
# oneway = yes
wayRelation.direction = DIRECTION.BACKWARD
self.assertTrue(wayRelation.is_prohibited)
wayRelation.direction = DIRECTION.FORWARD
self.assertFalse(wayRelation.is_prohibited)
# oneway non existing
wayRelation._one_way = None
self.assertFalse(wayRelation.is_one_way)
wayRelation.direction = DIRECTION.BACKWARD
self.assertFalse(wayRelation.is_prohibited)
def test_way_relation_distance_to_way_reflects_internal_value(self):
wayRelation = WayRelation(mockOSMWay_01_02_Loop)
wayRelation._distance_to_way = 10.
self.assertEqual(wayRelation.distance_to_way, 10.)
def test_way_relation_node_ahead(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
# ahead_ids is None on init
self.assertIsNone(wayRelation.node_ahead)
wayRelation.ahead_idx = 15
self.assertEqual(wayRelation.node_ahead, wayRelation.way.nodes[15])
def test_way_relation_last_node(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
# direction is NONE on init
self.assertIsNone(wayRelation.last_node)
# forward
wayRelation.direction = DIRECTION.FORWARD
self.assertEqual(wayRelation.last_node, wayRelation.way.nodes[-1])
# backward
wayRelation.direction = DIRECTION.BACKWARD
self.assertEqual(wayRelation.last_node, wayRelation.way.nodes[0])
def test_way_relation_last_node_coordinates(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
# direction is NONE on init
self.assertIsNone(wayRelation.last_node_coordinates)
# forward
wayRelation.direction = DIRECTION.FORWARD
coords = np.radians(np.array([wayRelation.way.nodes[-1].lat, wayRelation.way.nodes[-1].lon], dtype=float))
assert_array_almost_equal(wayRelation.last_node_coordinates, coords)
# backward
wayRelation.direction = DIRECTION.BACKWARD
coords = np.radians(np.array([wayRelation.way.nodes[0].lat, wayRelation.way.nodes[0].lon], dtype=float))
assert_array_almost_equal(wayRelation.last_node_coordinates, coords)
def test_way_relation_node_before_edge_coordinates(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
coords = wayRelation.node_before_edge_coordinates(0)
assert_array_almost_equal(coords, np.array([0., 0.]))
coords = wayRelation.node_before_edge_coordinates(wayRelation.way.nodes[0].id)
coords_e = np.radians(np.array([wayRelation.way.nodes[1].lat, wayRelation.way.nodes[1].lon], dtype=float))
assert_array_almost_equal(coords, coords_e)
coords = wayRelation.node_before_edge_coordinates(wayRelation.way.nodes[-1].id)
coords_e = np.radians(np.array([wayRelation.way.nodes[-2].lat, wayRelation.way.nodes[-2].lon], dtype=float))
assert_array_almost_equal(coords, coords_e)
def test_way_relation_split_no_matching_node(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
wrs = wayRelation.split(0)
self.assertEqual(len(wrs), 0)
def test_way_relation_split_use_correct_ids(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
wrs = wayRelation.split(wayRelation._nodes_ids[5], [-100, -200])
self.assertEqual(wrs[0].id, -100)
self.assertEqual(wrs[1].id, -200)
def test_way_relation_split_on_edge_node(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
edge_node_ids = wayRelation.edge_nodes_ids
for edge_node_id in edge_node_ids:
wrs = wayRelation.split(edge_node_id)
self.assertEqual(len(wrs), 1)
self.assertEqual(wrs[0], wayRelation)
self.assertEqual(wrs[0].way.tags, wayRelation.way.tags)
def test_way_relation_split_on_internal_node(self):
wayRelation = WayRelation(mockOSMWay_01_01_LongCurvy)
way_ids = [-10, -20]
for idx, node_id in enumerate(wayRelation._nodes_ids):
if idx == 0 or idx == len(wayRelation._nodes_ids) - 1:
continue
wrs = wayRelation.split(node_id, way_ids)
self.assertEqual(len(wrs), 2)
assert_array_almost_equal(wrs[0]._nodes_ids, wayRelation._nodes_ids[:idx + 1])
assert_array_almost_equal(wrs[1]._nodes_ids, wayRelation._nodes_ids[idx:])
self.assertIn(node_id, wrs[0].edge_nodes_ids)
self.assertIn(node_id, wrs[1].edge_nodes_ids)
self.assertEqual(wrs[0].way.tags, wayRelation.way.tags)
self.assertEqual(wrs[1].way.tags, wayRelation.way.tags)
self.assertEqual(way_ids, [wr.id for wr in wrs])
# Helpers
def make_wayRelation_location_dirty(self, wayRelation):
wayRelation.distance_to_node_ahead = 10.
wayRelation.location_rad = 0.8
wayRelation.bearing_rad = 2.
wayRelation.active = True
wayRelation.diverting = True
wayRelation.ahead_idx = 5
wayRelation.behind_idx = 4
wayRelation._active_bearing_delta = 3.
wayRelation._distance_to_way = 20.
def assert_wayRelation_variables_reset(self, wayRelation):
self.assertEqual(wayRelation.distance_to_node_ahead, 0.)
self.assertIsNone(wayRelation.location_rad)
self.assertIsNone(wayRelation.bearing_rad)
self.assertFalse(wayRelation.active)
self.assertFalse(wayRelation.diverting)
self.assertIsNone(wayRelation.ahead_idx)
self.assertIsNone(wayRelation.behind_idx)
self.assertIsNone(wayRelation._active_bearing_delta)
self.assertIsNone(wayRelation._distance_to_way)
def wayRelation_mid_point_rad(self, wayRelation):
return np.average(wayRelation.bbox, axis=0)
@@ -1,74 +0,0 @@
import unittest
from selfdrive.mapd.lib.WayRelationIndex import WayRelationIndex
from selfdrive.mapd.test.mock_data import mockWayCollection01
class TestWayRelationIndex(unittest.TestCase):
def test_init_and_add(self):
wrs = mockWayCollection01.way_relations
wr_index = WayRelationIndex(wrs)
# expected init logic, including add logic.
edge_nodes_index_dict = {}
full_nodes_index_dict = {}
for wr in wrs:
for node in wr.way.nodes:
node_id = node.id
full_nodes_index_dict[node_id] = full_nodes_index_dict.get(node_id, []) + [wr]
if node_id in wr.edge_nodes_ids:
edge_nodes_index_dict[node_id] = edge_nodes_index_dict.get(node_id, []) + [wr]
# assert logic delivers same result
self.assertDictEqual(edge_nodes_index_dict, wr_index._edge_nodes_index_dict)
self.assertDictEqual(full_nodes_index_dict, wr_index._full_nodes_index_dict)
self.assertEqual(len(wr_index._edge_nodes_index_dict), 586)
self.assertEqual(len(wr_index._full_nodes_index_dict), 2342)
def test_remove(self):
wrs = mockWayCollection01.way_relations
wr_index = WayRelationIndex(wrs)
wr_to_remove = wrs[0]
affected_full_node_ids = [nodesData.id for nodesData in wr_to_remove.way.nodes]
affected_edge_node_ids = wr_to_remove.edge_nodes_ids
initial_full_lists = [wr_index._full_nodes_index_dict[ndid] for ndid in affected_full_node_ids]
initial_edge_lists = [wr_index._edge_nodes_index_dict[ndid] for ndid in affected_edge_node_ids]
expected_final_full_lists = [[wr for wr in li if wr is not wr_to_remove] for li in initial_full_lists]
expected_final_edge_lists = [[wr for wr in li if wr is not wr_to_remove] for li in initial_edge_lists]
wr_index.remove(wr_to_remove)
final_full_lists = [wr_index._full_nodes_index_dict[ndid] for ndid in affected_full_node_ids]
final_edge_lists = [wr_index._edge_nodes_index_dict[ndid] for ndid in affected_edge_node_ids]
for idx, li in enumerate(final_full_lists):
self.assertListEqual(li, expected_final_full_lists[idx])
for idx, li in enumerate(final_edge_lists):
self.assertListEqual(li, expected_final_edge_lists[idx])
def test_way_relations_with_edge_node_id(self):
wr_index = WayRelationIndex([])
ref_dict = {
0: ["fake_wr1", "fake_wr2"],
1: ["fake_wr3"],
3: ["fake_wr4", "fake_wr5", "fake_wr6"],
}
wr_index._edge_nodes_index_dict = ref_dict
for key, li in ref_dict.items():
self.assertListEqual(li, wr_index.way_relations_with_edge_node_id(key))
def test_way_relations_with_node_id(self):
wr_index = WayRelationIndex([])
ref_dict = {
0: ["fake_wr1", "fake_wr2"],
1: ["fake_wr3"],
3: ["fake_wr4", "fake_wr5", "fake_wr6"],
}
wr_index._full_nodes_index_dict = ref_dict
for key, li in ref_dict.items():
self.assertListEqual(li, wr_index.way_relations_with_node_id(key))
-234
View File
@@ -1,234 +0,0 @@
import unittest
from selfdrive.mapd.lib.geo import vectors, ref_vectors, bearing_to_points, distance_to_points
import numpy as np
from numpy.testing import assert_array_almost_equal
from selfdrive.mapd.test.mock_data import mockNodesData01
class TestMapsdGeoLibrary(unittest.TestCase):
def test_vectors(self):
points = mockNodesData01.radians
expected = np.array([
[-1.34011951e-05, 1.00776468e-05],
[-5.83610920e-06, 4.41046897e-06],
[-7.83348567e-06, 5.94114032e-06],
[-7.08560788e-06, 5.30408795e-06],
[-6.57632550e-06, 4.05791838e-06],
[-1.16077872e-06, 6.91151252e-07],
[-1.53178098e-05, 9.62215139e-06],
[-5.76314175e-06, 3.55176643e-06],
[-1.61124141e-05, 9.86127759e-06],
[-1.48006628e-05, 8.58192512e-06],
[-1.72237209e-06, 1.60570482e-06],
[-8.68985228e-06, 9.22062311e-06],
[-1.42922812e-06, 1.51494711e-06],
[-3.39761486e-06, 2.57087743e-06],
[-2.75467373e-06, 1.28631255e-06],
[-1.57501989e-05, 5.72309451e-06],
[-2.52143954e-06, 1.34565295e-06],
[-1.65278643e-06, 1.28630942e-06],
[-2.22196114e-05, 1.64360838e-05],
[-5.88675934e-06, 4.08234746e-06],
[-1.83673390e-06, 1.46782408e-06],
[-1.55004206e-06, 1.51843800e-06],
[-1.20451533e-06, 2.06298011e-06],
[-1.91801338e-06, 4.64083285e-06],
[-2.38653483e-06, 5.60076524e-06],
[-1.65269781e-06, 5.78402290e-06],
[-3.66908309e-07, 2.75412965e-06],
[0.00000000e+00, 1.92858882e-06],
[9.09242615e-08, 2.66162711e-06],
[3.14490354e-07, 1.53065382e-06],
[8.66452477e-08, 4.83456208e-07],
[2.41750593e-07, 1.10828411e-06],
[7.43745228e-06, 1.27618831e-05],
[5.59968054e-06, 9.63947367e-06],
[2.01951467e-06, 2.75413219e-06],
[4.59952643e-07, 6.42281301e-07],
[1.74353749e-06, 1.74533121e-06],
[2.57144338e-06, 2.11185266e-06],
[1.46893187e-05, 1.11999169e-05],
[3.84659229e-05, 2.85527952e-05],
[2.71627936e-05, 1.98727946e-05],
[8.44632540e-06, 6.15058628e-06],
[2.29420323e-06, 1.92859222e-06],
[2.58083439e-06, 3.16952222e-06],
[3.76373643e-06, 5.14174911e-06],
[5.32416098e-06, 6.51707770e-06],
[8.62890928e-06, 1.11998258e-05],
[1.25762497e-05, 1.65231340e-05],
[8.90452991e-06, 1.10148240e-05],
[4.86505726e-06, 4.59023120e-06],
[3.85545276e-06, 3.39642031e-06],
[3.48753893e-06, 3.30566145e-06],
[2.99557303e-06, 2.61276368e-06],
[2.15496788e-06, 1.87797727e-06],
[4.10564937e-06, 3.58142649e-06],
[1.53680853e-06, 1.33866906e-06],
[4.99540175e-06, 4.35635790e-06],
[1.37744970e-06, 1.19380643e-06],
[1.74319821e-06, 1.28456429e-06],
[9.99931238e-07, 1.14493663e-06],
[6.42735560e-07, 1.19380547e-06],
[3.66818436e-07, 1.46782199e-06],
[5.45413874e-08, 1.83783170e-06],
[-1.35818548e-07, 1.14842666e-06],
[-5.50758101e-07, 3.02989178e-06],
[-4.58785270e-07, 2.66162724e-06],
[-2.51315555e-07, 1.19031459e-06],
[-3.91409773e-07, 1.65457223e-06],
[-2.14525206e-06, 5.67755902e-06],
[-4.24558096e-07, 1.39102753e-06],
[-1.46936730e-06, 5.32325561e-06],
[-1.37632061e-06, 4.59021715e-06],
[-8.26642899e-07, 4.68097349e-06],
[-6.42702724e-07, 4.95673534e-06],
[-3.66796960e-07, 7.25009780e-06],
[-1.82861669e-07, 8.99542699e-06],
[4.09564134e-07, 6.11214315e-06],
[7.80629912e-08, 1.45734993e-06],
[4.81205526e-07, 7.56076647e-06],
[2.01036346e-07, 2.42775302e-06]])
v = vectors(points)
assert_array_almost_equal(v, expected)
def test_ref_vectors(self):
points = mockNodesData01.radians
expected = np.array([
[1.59924145e-04, -1.07153714e-04],
[1.46520873e-04, -9.70788297e-05],
[1.40683931e-04, -9.26694631e-05],
[1.32849368e-04, -8.67297434e-05],
[1.25762852e-04, -8.14268689e-05],
[1.19185869e-04, -7.73700167e-05],
[1.18024984e-04, -7.66790438e-05],
[1.02705711e-04, -6.70592230e-05],
[9.69420991e-05, -6.35082196e-05],
[8.08284530e-05, -5.36489556e-05],
[6.60268961e-05, -4.50685727e-05],
[6.43043874e-05, -4.34630144e-05],
[5.56137708e-05, -3.42431117e-05],
[5.41844341e-05, -3.27282671e-05],
[5.07866397e-05, -3.01576270e-05],
[4.80318817e-05, -2.88714948e-05],
[3.22813286e-05, -2.31493755e-05],
[2.97598330e-05, -2.18038275e-05],
[2.81069973e-05, -2.05175815e-05],
[5.88679032e-06, -4.08230278e-06],
[0.00000000e+00, 0.00000000e+00],
[-1.83673390e-06, 1.46782408e-06],
[-3.38677236e-06, 2.98626574e-06],
[-4.59127869e-06, 5.04925111e-06],
[-6.50926460e-06, 9.69009532e-06],
[-8.89575243e-06, 1.52908806e-05],
[-1.05483839e-05, 2.10749224e-05],
[-1.09152548e-05, 2.38290571e-05],
[-1.09152276e-05, 2.57576459e-05],
[-1.08242659e-05, 2.84192717e-05],
[-1.05097542e-05, 2.99499212e-05],
[-1.04231024e-05, 3.04333762e-05],
[-1.01813369e-05, 3.15416571e-05],
[-2.74371711e-06, 4.43034426e-05],
[2.85599752e-06, 5.39428964e-05],
[4.87550206e-06, 5.66970360e-05],
[5.33545066e-06, 5.73393202e-05],
[7.07897615e-06, 5.90846634e-05],
[9.65040026e-06, 6.11965396e-05],
[2.43395796e-05, 7.23966392e-05],
[6.28046063e-05, 1.00950641e-04],
[8.99657904e-05, 1.20825635e-04],
[9.84114021e-05, 1.26977201e-04],
[1.00705361e-04, 1.28906084e-04],
[1.03285783e-04, 1.32075942e-04],
[1.07048835e-04, 1.37218192e-04],
[1.12372096e-04, 1.43736004e-04],
[1.20999382e-04, 1.54937080e-04],
[1.33573053e-04, 1.71462176e-04],
[1.42475686e-04, 1.82478533e-04],
[1.47339899e-04, 1.87069658e-04],
[1.51194707e-04, 1.90466811e-04],
[1.54681601e-04, 1.93773152e-04],
[1.57676653e-04, 1.96386513e-04],
[1.59831239e-04, 1.98264929e-04],
[1.63936150e-04, 2.01847201e-04],
[1.65472675e-04, 2.03186195e-04],
[1.70467147e-04, 2.07543619e-04],
[1.71844334e-04, 2.08737728e-04],
[1.73587247e-04, 2.10022678e-04],
[1.74586922e-04, 2.11167839e-04],
[1.75229389e-04, 2.12361789e-04],
[1.75595876e-04, 2.13829694e-04],
[1.75650001e-04, 2.15667538e-04],
[1.75513922e-04, 2.16815933e-04],
[1.74962478e-04, 2.19845700e-04],
[1.74503092e-04, 2.22507224e-04],
[1.74251509e-04, 2.23697482e-04],
[1.73859727e-04, 2.25351966e-04],
[1.71713202e-04, 2.31029044e-04],
[1.71288336e-04, 2.32419977e-04],
[1.69817793e-04, 2.37742908e-04],
[1.68440467e-04, 2.42332824e-04],
[1.67612807e-04, 2.47013617e-04],
[1.66969033e-04, 2.51970213e-04],
[1.66600674e-04, 2.59220232e-04],
[1.66415880e-04, 2.68215619e-04],
[1.66824132e-04, 2.74327850e-04],
[1.66901881e-04, 2.75785216e-04],
[1.67381459e-04, 2.83346086e-04],
[1.67581971e-04, 2.85773882e-04]])
v = ref_vectors(points[20], points)
assert_array_almost_equal(v, expected)
def test_bearing_to_points(self):
points = mockNodesData01.radians
expected = np.array([
2.16112265, 2.15595027, 2.15326799, 2.14916735, 2.14538642,
2.14657678, 2.14694997, 2.1492257, 2.1507589, 2.15676899,
2.16973441, 2.1651606, 2.12270237, 2.11416356, 2.10665211,
2.11201708, 2.19291574, 2.2031069, 2.20136186, 2.17712517,
0., -0.8965745, -0.84815954, -0.73792895, -0.59150953,
-0.5269061, -0.46406215, -0.42954043, -0.4008254, -0.36391371,
-0.33748609, -0.32996807, -0.31223189, -0.06185112, 0.05289544,
0.08578116, 0.0927833, 0.11924233, 0.15640718, 0.32432622,
0.55653415, 0.64003094, 0.6593301, 0.66319086, 0.66367982,
0.66251077, 0.66354137, 0.66302176, 0.66181884, 0.66291139,
0.66714676, 0.67095594, 0.67367984, 0.6765003, 0.67847961,
0.68212344, 0.68345356, 0.68762778, 0.68876073, 0.69070183,
0.69085143, 0.68988665, 0.68753177, 0.68348884, 0.68051081,
0.67220053, 0.66506824, 0.66177969, 0.65712162, 0.63916951,
0.6351146, 0.62025347, 0.60741567, 0.59618923, 0.58521935,
0.57122582, 0.55532475, 0.54636839, 0.54422542, 0.53357655,
0.53037033])
v = bearing_to_points(points[20], points)
assert_array_almost_equal(v, expected)
def test_distance_to_points(self):
points = mockNodesData01.radians
expected = np.array([
1226.82569068, 1120.13820773, 1073.61121415, 1011.10016574,
954.81557436, 905.58045038, 896.97734399, 781.7102819,
738.58271117, 618.26145463, 509.47052142, 494.6403804,
416.22483123, 403.42108699, 376.42615499, 357.15106681,
253.15957483, 235.11572972, 221.77439728, 45.65465979,
0., 14.98414, 28.77606056, 43.49299446,
74.39463425, 112.74005248, 150.19482607, 167.03665191,
178.28443483, 193.80834084, 202.28154097, 205.01173833,
211.22777104, 282.88676739, 344.25957352, 362.66370657,
367.00206795, 379.23951996, 394.82505328, 486.76073331,
757.70254732, 960.03439155, 1023.81434529, 1042.49401713,
1068.53770096, 1109.12696535, 1162.74555108, 1252.847351,
1385.17179405, 1475.42502599, 1517.57849916, 1549.79838056,
1580.12405964, 1605.05483058, 1622.98937809, 1657.19268821,
1669.99157205, 1711.63883132, 1723.09133393, 1736.47655688,
1746.16073119, 1754.63481838, 1763.34186103, 1772.62691273,
1777.76189094, 1790.62024447, 1802.11488235, 1807.1040605,
1813.90756815, 1834.49265566, 1840.00708445, 1861.96087374,
1880.81678093, 1902.42091191, 1926.37194131, 1963.78301115,
2011.62679077, 2046.18028824, 2054.37811294, 2097.30347724,
2111.28586072])
v = distance_to_points(points[20], points)
assert_array_almost_equal(v, expected)
+162
View File
@@ -0,0 +1,162 @@
import json
import time
import platform
import numpy as np
from openpilot.common.params_pyx import Params
from openpilot.selfdrive.controls.lib.alertmanager import set_offroad_alert
from openpilot.selfdrive.navd.helpers import Coordinate
from openpilot.selfdrive.sunnypilot.live_map_data import QUERY_RADIUS
from openpilot.common.realtime import Ratekeeper, set_core_affinity
from openpilot.system.swaglog import cloudlog
import os
import glob
import shutil
from openpilot.selfdrive.sunnypilot.live_map_data.osm_map_data import OsmMapData
from openpilot.selfdrive.sunnypilot.live_map_data import R
# PFEIFER - MAPD {{
params = Params()
mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params
# }} PFEIFER - MAPD
COMMON_DIR = '/data/media/0/osm'
MAPD_BIN_DIR = '/data/openpilot/third_party/pfeiferj-mapd'
MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd')
def get_files_for_cleanup():
paths = [
f"{COMMON_DIR}/db",
f"{COMMON_DIR}/v*"
]
files_to_remove = []
for path in paths:
if os.path.exists(path):
files = glob.glob(path + '/**', recursive=True)
files_to_remove.extend(files)
# check for version and mapd files
if not os.path.isfile(MAPD_PATH):
files_to_remove.append(MAPD_PATH)
return files_to_remove
def cleanup_OLD_OSM_data(files_to_remove):
for file in files_to_remove:
# Remove trailing slash if path is file
if file.endswith('/') and os.path.isfile(file[:-1]):
file = file[:-1]
# Try to remove as file or symbolic link first
if os.path.islink(file) or os.path.isfile(file):
os.remove(file)
elif os.path.isdir(file): # If it's a directory
shutil.rmtree(file, ignore_errors=False)
def _get_current_bounding_box(self, radius: float):
self.last_query_radius = radius
# Calculate the bounding box coordinates for the bbox containing the circle around location.
bbox_angle = float(np.degrees(radius / R))
lat = float(self.last_gps.latitude)
lon = float(self.last_gps.longitude)
return {
"min_lat": lat - bbox_angle,
"min_lon": lon - bbox_angle,
"max_lat": lat + bbox_angle,
"max_lon": lon + bbox_angle,
}
def _request_refresh_osm_bounds_data(self):
self.last_refresh_loc = Coordinate(self.last_gps.latitude, self.last_gps.longitude)
self.last_query_radius = QUERY_RADIUS
current_bounding_box = self._get_current_bounding_box(self.last_query_radius)
mem_params.put("OSMDownloadBounds", json.dumps(current_bounding_box))
def request_refresh_osm_location_data(nations: [str], states: [str] = None):
params.put("OsmDownloadedDate", str(time.time()))
params.put_bool("OsmDbUpdatesCheck", False)
osm_download_locations = json.dumps({
"nations": nations,
"states": states or []
})
print(f"Downloading maps for {osm_download_locations}")
mem_params.put("OSMDownloadLocations", osm_download_locations)
def filter_nations_and_states(nations: [str], states: [str] = None):
"""Filters and prepares nation and state data for OSM map download.
If the nation is 'US' and a specific state is provided, the nation 'US' is removed from the list.
If the nation is 'US' and the state is 'All', the 'All' is removed from the list.
The idea behind these filters is that if a specific state in the US is provided,
there's no need to download map data for the entire US. Conversely,
if the state is unspecified (i.e., 'All'), we intend to download map data for the whole US,
and 'All' isn't a valid state name, so it's removed.
Parameters:
nations (list): A list of nations for which the map data is to be downloaded.
states (list, optional): A list of states for which the map data is to be downloaded. Defaults to None.
Returns:
tuple: Two lists. The first list is filtered nations and the second list is filtered states.
"""
if "US" in nations and states and not any(x.lower() == "all" for x in states):
# If a specific state in the US is provided, remove 'US' from nations
nations.remove("US")
elif "US" in nations and states and any(x.lower() == "all" for x in states):
# If 'All' is provided as a state (case invariant), remove those instances from states
states = [x for x in states if x.lower() != "all"]
elif "US" not in nations and states and any(x.lower() == "all" for x in states):
states.remove("All")
return nations, states or []
def update_osm_db():
# last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0)
# if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60
if params.get_bool("OsmDbUpdatesCheck"):
cleanup_OLD_OSM_data(get_files_for_cleanup())
country = params.get('OsmLocationName', encoding='utf-8')
state = params.get('OsmStateName', encoding='utf-8') or "All"
filtered_nations, filtered_states = filter_nations_and_states([country], [state])
request_refresh_osm_location_data(filtered_nations, filtered_states)
if not mem_params.get("OSMDownloadBounds"):
mem_params.put("OSMDownloadBounds", "")
if not mem_params.get("LastGPSPosition"):
mem_params.put("LastGPSPosition", "{}")
def main_thread(sm=None, pm=None):
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("mapd: failed to set core affinity")
rk = Ratekeeper(1, print_delay_threshold=None)
live_map_sp = OsmMapData()
while True:
show_alert = get_files_for_cleanup() and params.get_bool("OsmLocal")
set_offroad_alert("Offroad_OSMUpdateRequired", show_alert, "This alert will be cleared when new maps are downloaded.")
update_osm_db()
live_map_sp.tick()
rk.keep_time()
def main():
main_thread()
if __name__ == "__main__":
main()
@@ -0,0 +1,18 @@
from cereal import custom
from openpilot.system.swaglog import cloudlog
LOOK_AHEAD_HORIZON_TIME = 15. # s. Time horizon for look ahead of turn speed sections to provide on liveMapDataSP msg.
_DEBUG = False
_CLOUDLOG_DEBUG = False
ROAD_NAME_TIMEOUT = 30 # secs
DataType = custom.LiveMapDataSP.DataType
R = 6373000.0 # approximate radius of earth in mts
QUERY_RADIUS = 3000 # mts. Radius to use on OSM data queries.
QUERY_RADIUS_OFFLINE = 2250 # mts. Radius to use on offline OSM data queries.
def get_debug(msg, log_to_cloud=True):
if _CLOUDLOG_DEBUG and log_to_cloud:
cloudlog.debug(msg)
if _DEBUG:
print(msg)
@@ -0,0 +1,106 @@
import math
from abc import abstractmethod, ABC
from cereal import custom, messaging
from openpilot.selfdrive.navd.helpers import Coordinate
from openpilot.selfdrive.sunnypilot.live_map_data import get_debug
class BaseMapData(ABC):
def __init__(self):
self._last_gps: Coordinate | None = None
self._gps_sock = None
self._data_type = custom.LiveMapDataSP.DataType.default
self._sub_master = messaging.SubMaster(['liveLocationKalman', 'carControl'])
self._pub_master = messaging.PubMaster(['liveMapDataSP'])
@abstractmethod
def update_location(self, current_location: Coordinate):
pass
@abstractmethod
def get_current_speed_limit(self) -> float:
pass
@abstractmethod
def get_next_speed_limit_and_distance(self) -> (float, float):
pass
@abstractmethod
def get_current_road_name(self) -> str:
pass
def _is_gps_data_valid(self) -> bool:
all_sock_alive = self._sub_master.all_alive(service_list=[self._gps_sock])
all_sock_valid = self._sub_master.all_valid(service_list=[self._gps_sock])
return all_sock_alive and all_sock_valid
def get_current_location(self) -> Coordinate | None:
self._gps_sock = "liveLocationKalman"
if not self._sub_master.updated[self._gps_sock] or not self._sub_master.valid[self._gps_sock]:
return None
_last_gps = self._sub_master[self._gps_sock]
# ignore the message if the fix is invalid
if not _last_gps.positionGeodetic.valid:
return None
kalman_bearing_deg = math.degrees(_last_gps.calibratedOrientationNED.value[2])
kalman_speed = _last_gps.velocityCalibrated.value[2]
kalman_latitude = _last_gps.positionGeodetic.value[0];
kalman_longitude = _last_gps.positionGeodetic.value[1]
result = Coordinate(kalman_latitude, kalman_longitude)
result.annotations['unixTimestampMillis'] = _last_gps.unixTimestampMillis
result.annotations['speed'] = kalman_speed
result.annotations['bearingDeg'] = kalman_bearing_deg
result.annotations['accuracy'] = 1 # Hardcoded since liveLocationKalman does not report this.
result.annotations['bearingAccuracyDeg'] = 1. # you'll need to assign this if available
return result
def get_live_map_data_sp(self, speed_limit, next_speed_limit, next_speed_limit_distance, current_road_name):
last_gps = self.get_current_location()
map_data_msg = messaging.new_message('liveMapDataSP')
map_data_msg.valid = self._is_gps_data_valid()
live_map_data = map_data_msg.liveMapDataSP
if last_gps:
live_map_data.lastGpsTimestamp = last_gps.annotations.get('unixTimestampMillis', 0)
live_map_data.lastGpsLatitude = last_gps.latitude
live_map_data.lastGpsLongitude = last_gps.longitude
live_map_data.lastGpsSpeed = last_gps.annotations.get('speed', 0)
live_map_data.lastGpsBearingDeg = last_gps.annotations.get('bearingDeg', 0)
live_map_data.lastGpsAccuracy = last_gps.annotations.get('accuracy', 0)
live_map_data.lastGpsBearingAccuracyDeg = last_gps.annotations.get('bearingAccuracyDeg', 0)
live_map_data.speedLimitValid = bool(speed_limit > 0)
live_map_data.speedLimit = speed_limit
live_map_data.speedLimitAheadValid = bool(next_speed_limit > 0)
live_map_data.speedLimitAhead = float(next_speed_limit)
live_map_data.speedLimitAheadDistance = float(next_speed_limit_distance)
live_map_data.currentRoadName = str(current_road_name)
live_map_data.dataType = self._data_type
return map_data_msg
def publish(self):
speed_limit = self.get_current_speed_limit()
current_road_name = self.get_current_road_name()
next_speed_limit, next_speed_limit_distance = self.get_next_speed_limit_and_distance()
live_map_data_sp = self.get_live_map_data_sp(
speed_limit,
next_speed_limit,
next_speed_limit_distance,
current_road_name
)
self._pub_master.send('liveMapDataSP', live_map_data_sp)
get_debug(f"SRC: [{self.__class__.__name__}] | SLC: [{speed_limit}] | NSL: [{next_speed_limit}] | NSLD: [{next_speed_limit_distance}] | CRN: [{current_road_name}] | GPS: [{self._last_gps}] Annotations: [{', '.join(f'{key}: {value}' for key, value in self._last_gps.annotations.items()) if self._last_gps else []}]")
def tick(self):
self._sub_master.update()
self._last_gps = self.get_current_location()
self.update_location(self._last_gps)
self.publish()
@@ -0,0 +1,50 @@
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from cereal import messaging
from openpilot.common.realtime import set_core_affinity
from openpilot.selfdrive.controls.lib.sunnypilot.common import Policy
from openpilot.selfdrive.controls.lib.sunnypilot.speed_limit_resolver import SpeedLimitResolver
from openpilot.selfdrive.sunnypilot.live_map_data import get_debug
from openpilot.system.swaglog import cloudlog
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("mapd: failed to set core affinity")
while True:
live_map_data_sp_thread_debug()
def live_map_data_sp_thread_debug():
_sub_master = messaging.SubMaster(['carState', 'navInstruction', 'liveLocationKalman', 'liveMapDataSP', 'longitudinalPlanSP'])
_sub_master.update()
v_ego = _sub_master['carState'].vEgo
long_spl = _sub_master['longitudinalPlanSP'].speedLimit
_policy = Policy.car_state_priority
_resolver = SpeedLimitResolver(_policy)
_speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master)
print(_speed_limit, _distance, _source, " <-> ", long_spl)
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()
@@ -0,0 +1,50 @@
import json
import platform
from openpilot.common.params_pyx import Params
from openpilot.selfdrive.navd.helpers import Coordinate
from openpilot.selfdrive.sunnypilot.live_map_data import DataType
from openpilot.selfdrive.sunnypilot.live_map_data.base_map_data import BaseMapData
class OsmMapData(BaseMapData):
def __init__(self):
super().__init__()
self.last_gps = Coordinate(0.0, 0.0)
self.params = Params()
self.mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else self.params
self.data_type = DataType.offline
def update_location(self, current_location):
self.last_gps = current_location
if not self.last_gps:
return
last_gps_position_for_osm = {
"latitude": self.last_gps.latitude,
"longitude": self.last_gps.longitude,
"bearing": self.last_gps.annotations.get("bearingDeg", 0)
}
self.mem_params.put("LastGPSPosition", json.dumps(last_gps_position_for_osm))
def get_current_speed_limit(self):
speed_limit = self.mem_params.get("MapSpeedLimit", encoding='utf8')
return float(speed_limit) if speed_limit else 0.0
def get_current_road_name(self):
current_road_name = self.mem_params.get("RoadName", encoding='utf8')
return current_road_name if current_road_name else ""
def get_next_speed_limit_and_distance(self):
next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8')
next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {}
next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0)
next_speed_limit_latitude = next_speed_limit_section.get('latitude')
next_speed_limit_longitude = next_speed_limit_section.get('longitude')
next_speed_limit_distance = 0
if next_speed_limit_latitude and next_speed_limit_longitude:
next_speed_limit_coordinates = Coordinate(next_speed_limit_latitude, next_speed_limit_longitude)
next_speed_limit_distance = (self.last_gps or Coordinate(0, 0)).distance_to(next_speed_limit_coordinates)
return next_speed_limit, next_speed_limit_distance
@@ -0,0 +1,40 @@
# DISCLAIMER: This code is intended principally for development and debugging purposes.
# Although it provides a standalone entry point to the program, users should refer
# to the actual implementations for consumption. Usage outside of development scenarios
# is not advised and could lead to unpredictable results.
import threading
import traceback
from openpilot.common.realtime import Ratekeeper, set_core_affinity
from openpilot.selfdrive.sunnypilot.live_map_data import get_debug
from openpilot.selfdrive.sunnypilot.live_map_data.osm_map_data import OsmMapData
from openpilot.system.swaglog import cloudlog
def excepthook(args):
get_debug(f'MapD: Threading exception:\n{args}')
traceback.print_exception(args.exc_type, args.exc_value, args.exc_traceback)
def live_map_data_sp_thread():
try:
set_core_affinity([0, 1, 2, 3])
except Exception:
cloudlog.exception("mapd: failed to set core affinity")
live_map_sp = OsmMapData()
rk = Ratekeeper(1, print_delay_threshold=None)
while True:
live_map_sp.tick()
rk.keep_time()
def main():
threading.excepthook = excepthook
live_map_data_sp_thread()
if __name__ == "__main__":
main()
@@ -0,0 +1,52 @@
#pragma once
#include <QJsonObject>
#include <algorithm> // for std::sort
#include <deque>
#include <QDir>
#include "selfdrive/ui/qt/offroad/sunnypilot/json_fetcher.h"
static const std::tuple<QString, QString> defaultLocation = std::make_tuple("== None ==", "");
// New class LocationsFetcher that handles web requests and JSON parsing
class LocationsFetcher {
public:
inline std::vector<std::tuple<QString, QString, QString, QString>>
getLocationsFromURL(const QUrl &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
// Initialize an empty vector to hold the locations
std::vector<std::tuple<QString, QString, QString, QString>> locations;
JsonFetcher fetcher;
QJsonObject json = fetcher.getJsonFromURL(url.toString());
for (auto it = json.begin(); it != json.end(); ++it) {
QString code = it.key();
QJsonObject obj = it.value().toObject();
QString fullName = obj["full_name"].toString();
locations.push_back(std::make_tuple(fullName, code, QString(), QString()));
}
// Sort locations by full name
std::sort(locations.begin(), locations.end(), [](const auto &lhs, const auto &rhs) {
return std::get < 0 > (lhs) < std::get < 0 > (rhs); // Compare full names
});
// Optionally, you can now add defaultName entry at the beginning
locations.insert(locations.begin(), std::tuple_cat(customLocation, std::make_tuple("", "")));
return locations;
}
inline std::vector<std::tuple<QString, QString, QString, QString>>
getLocationsFromURL(const QString &url, const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL(QUrl(url), customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString>>
getOsmLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL("https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/nation_bounding_boxes.json", customLocation);
}
inline std::vector<std::tuple<QString, QString, QString, QString>>
getUsStatesLocations(const std::tuple<QString, QString> &customLocation = defaultLocation) const {
return getLocationsFromURL("https://raw.githubusercontent.com/pfeiferj/openpilot-mapd/main/us_states_bounding_boxes.json", customLocation);
}
};
@@ -3,107 +3,266 @@
OsmPanel::OsmPanel(QWidget *parent) : QFrame(parent) {
main_layout = new QStackedLayout(this);
ListWidget *list = new ListWidget(this, false);
// param, title, desc, icon
std::vector<std::tuple<QString, QString, QString, QString>> toggle_defs{
{
"OsmLocalDb",
tr("Use Offline Database"),
"",
"../assets/offroad/icon_blank.png",
}
};
for (auto &[param, title, desc, icon] : toggle_defs) {
auto toggle = new ParamControl(param, title, desc, icon, this);
list->addItem(toggle);
toggles[param.toStdString()] = toggle;
}
osmUpdateLbl = new QLabel(tr("Database updates can be downloaded while the car is off."));
osmUpdateLbl->setStyleSheet("font-size: 50px; font-weight: 400; text-align: left; padding-top: 30px; padding-bottom: 30px;");
list->addItem(osmUpdateLbl);
osmUpdateBtn = new ButtonControl(tr("OpenStreetMap Database Update"), tr("CHECK"));
connect(osmUpdateBtn, &ButtonControl::clicked, [=]() {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot and perform a database update?\nEstimated time: 30-90 minutes"), tr("Reboot"), parent)) {
params.putBool("OsmDbUpdatesCheck", true);
Hardware::reboot();
}
});
list->addItem(osmUpdateBtn);
osmDownloadBtn = new ButtonControl(tr("OpenStreetMap Database"), tr("SELECT"));
connect(osmDownloadBtn, &ButtonControl::clicked, [=]() {
std::vector<std::tuple<QString, QString, QString, QString>> locations = getOsmLocations();
QString initTitle = QString::fromStdString(params.get("OsmLocationTitle"));
QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle;
QStringList locationTitles;
for (auto& loc : locations) {
locationTitles.push_back(std::get<0>(loc));
}
QString selection = MultiOptionDialog::getSelection(tr("Select your location"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmLocal", "1");
params.put("OsmLocationTitle", selection.toStdString());
for (auto& loc : locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmLocationName", std::get<1>(loc).toStdString());
params.put("OsmWayTest", std::get<2>(loc).toStdString());
params.put("OsmLocationUrl", std::get<3>(loc).toStdString());
break;
}
}
osmDownloadBtn->setValue(selection);
if (selection != "== None ==") {
if (ConfirmationDialog::confirm(tr("Are you sure you want to reboot to start downloading the selected database? Estimated time: 30-90 minutes"), tr("Reboot"), parent)) {
params.putBool("OsmDbUpdatesCheck", true);
Hardware::reboot();
}
}
}
updateLabels();
});
list->addItem(osmDownloadBtn);
const auto list = new ListWidget(this, false);
list->addItem(mapdVersion = new LabelControl(tr("Mapd Version"), "Loading..."));
list->addItem(setupOsmDeleteMapsButton(parent));
list->addItem(offlineMapsETA = new LabelControl(tr("Offline Maps ETA"), ""));
list->addItem(offlineMapsElapsed = new LabelControl(tr("Time Elapsed"), ""));
list->addItem(setupOsmUpdateButton(parent));
list->addItem(setupOsmDownloadButton(parent));
list->addItem(setupUsStatesButton(parent));
connect(uiState(), &UIState::offroadTransition, [=](bool offroad) {
is_onroad = !offroad;
updateLabels();
});
timer = new QTimer(this);
connect(timer, &QTimer::timeout, this, QOverload<>::of(&OsmPanel::updateLabels));
timer->start(FAST_REFRESH_INTERVAL); // Time specified in milliseconds.
updateLabels();
osmScreen = new QWidget(this);
QVBoxLayout* vlayout = new QVBoxLayout(osmScreen);
auto *vlayout = new QVBoxLayout(osmScreen);
vlayout->setContentsMargins(50, 20, 50, 20);
vlayout->addWidget(new ScrollView(list, this), 1);
main_layout->addWidget(osmScreen);
}
void OsmPanel::showEvent(QShowEvent *event) {
updateLabels();
ButtonControl *OsmPanel::setupOsmDeleteMapsButton(QWidget *parent) {
osmDeleteMapsBtn = new ButtonControl(tr("Downloaded Maps"), tr("Delete Maps")); // Updated on updateLabels()
connect(osmDeleteMapsBtn, &ButtonControl::clicked, [=]() {
if (showConfirmationDialog(parent, "This will delete ALL downloaded maps\n\nAre you sure you want to delete all the maps?", "Yes, delete all the maps.")) {
QtConcurrent::run([=]() {
QDir dir(MAP_PATH);
osmDeleteMapsBtn->setEnabled(false);
osmDeleteMapsBtn->setText("Deleting...");
dir.removeRecursively();
updateMapSize();
osmDeleteMapsBtn->setEnabled(true);
osmDeleteMapsBtn->setText("DELETE");
});
updateLabels();
}
});
return osmDeleteMapsBtn;
}
ButtonControl *OsmPanel::setupOsmUpdateButton(QWidget *parent) {
osmUpdateBtn = new ButtonControl(tr("Database Update"), tr("CHECK")); // Updated on updateLabels()
connect(osmUpdateBtn, &ButtonControl::clicked, [=]() {
if (osm_download_in_progress && !download_failed_state) {
updateLabels();
} else if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
});
return osmUpdateBtn;
}
ButtonControl *OsmPanel::setupOsmDownloadButton(QWidget *parent) {
osmDownloadBtn = new ButtonControl(tr("Country"), tr("SELECT"));
connect(osmDownloadBtn, &ButtonControl::clicked, [=]() {
osmDownloadBtn->setEnabled(false);
osmDownloadBtn->setValue("Fetching Country list...");
const std::vector<std::tuple<QString, QString, QString, QString>> locations = getOsmLocations();
osmDownloadBtn->setEnabled(true);
osmDownloadBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmLocationTitle"));
const QString currentTitle = ((initTitle == "== None ==") || (initTitle.length() == 0)) ? "== None ==" : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("Country"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmLocal", "1");
params.put("OsmLocationTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmLocationName", std::get<1>(loc).toStdString());
break;
}
}
if (params.get("OsmLocationName") == "US") {
usStatesBtn->click();
return;
} else if (selection != "== None ==") {
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
}
updateLabels();
});
return osmDownloadBtn;
}
ButtonControl *OsmPanel::setupUsStatesButton(QWidget *parent) {
usStatesBtn = new ButtonControl(tr("State"), tr("SELECT"));
connect(usStatesBtn, &ButtonControl::clicked, [=]() {
const std::tuple<QString, QString> allStatesOption = std::make_tuple("All States (~4.8 GB)", "All");
usStatesBtn->setEnabled(false);
usStatesBtn->setValue("Fetching State list...");
const std::vector<std::tuple<QString, QString, QString, QString>> locations = getUsStatesLocations(allStatesOption);
usStatesBtn->setEnabled(true);
usStatesBtn->setValue("");
const QString initTitle = QString::fromStdString(params.get("OsmStateTitle"));
const QString currentTitle = ((initTitle == std::get<0>(allStatesOption)) || (initTitle.length() == 0)) ? "All" : initTitle;
QStringList locationTitles;
for (auto &loc: locations) {
locationTitles.push_back(std::get<0>(loc));
}
const QString selection = MultiOptionDialog::getSelection(tr("State"), locationTitles, currentTitle, this);
if (!selection.isEmpty()) {
params.put("OsmStateTitle", selection.toStdString());
for (auto &loc: locations) {
if (std::get<0>(loc) == selection) {
params.put("OsmStateName", std::get<1>(loc).toStdString());
break;
}
}
usStatesBtn->setValue(selection);
if (showConfirmationDialog(parent)) {
osm_download_in_progress = true;
params.putBool("OsmDbUpdatesCheck", true);
updateLabels();
}
}
updateLabels();
});
usStatesBtn->setVisible(false); // initially hidden
return usStatesBtn;
}
void OsmPanel::showEvent(QShowEvent *event) {
updateLabels(); // For snappier feeling
if (!timer->isActive()) {
timer->start(FAST_REFRESH_INTERVAL);
}
}
void OsmPanel::hideEvent(QHideEvent *event) {
if (timer->isActive()) {
timer->stop();
}
}
void OsmPanel::updateLabels() {
if (!isVisible()) {
return;
}
mapd_version = params.get("MapdVersion");
mapdVersion->setText(mapd_version.c_str());
QString name = QString::fromStdString(params.get("OsmLocationName"));
if (!name.isEmpty()) {
osmUpdateBtn->setVisible(!is_onroad);
updateMapSize();
osm_download_locations = mem_params.get("OSMDownloadLocations");
osm_download_in_progress = !osm_download_locations.empty();
timer->setInterval(osm_download_in_progress ? FAST_REFRESH_INTERVAL : SLOW_REFRESH_INTERVAL);
LOGT("Timer Interval %d", timer->interval());
const std::string osmLastDownloadTimeStr = params.get("OsmDownloadedDate");
if (!lastDownloadedTimePoint.has_value() && !osmLastDownloadTimeStr.empty()) {
const double osmLastDownloadTime = std::stod(osmLastDownloadTimeStr);
lastDownloadedTimePoint = std::chrono::system_clock::from_time_t(static_cast<std::time_t>(osmLastDownloadTime));
}
osmDownloadBtn->setEnabled(!osm_download_in_progress);
usStatesBtn->setEnabled(!osm_download_in_progress);
updateDownloadProgress();
const QString locationName = QString::fromStdString(params.get("OsmLocationName"));
const bool isUs = !locationName.isEmpty() && locationName == "US";
usStatesBtn->setVisible(isUs);
if (!locationName.isEmpty()) {
if (!isUs) {
params.remove("OsmStateName");
params.remove("OsmStateTitle");
}
osmUpdateBtn->setVisible(true);
} else {
params.remove("OsmLocal");
params.remove("OsmLocationName");
params.remove("OsmLocationTitle");
params.remove("OsmStateName");
params.remove("OsmStateTitle");
osmUpdateBtn->setVisible(false);
usStatesBtn->setVisible(false);
}
osmUpdateLbl->setVisible(is_onroad);
osmDownloadBtn->setEnabled(!is_onroad);
osmDownloadBtn->setValue(QString::fromStdString(params.get("OsmLocationTitle")));
usStatesBtn->setValue(QString::fromStdString(params.get("OsmStateTitle")));
update();
}
void OsmPanel::updateDownloadProgress() {
const auto pending_update_check = params.getBool("OsmDbUpdatesCheck");
const QJsonObject osmDownloadProgress = QJsonDocument::fromJson(params.get("OSMDownloadProgress").c_str()).object();
if (osm_download_in_progress && lastDownloadedTimePoint.has_value()) {
offlineMapsETA->setVisible(true);
offlineMapsElapsed->setVisible(true);
offlineMapsETA->setText(calculateETA(osmDownloadProgress, lastDownloadedTimePoint.value()));
offlineMapsElapsed->setText(calculateElapsedTime(osmDownloadProgress, lastDownloadedTimePoint.value()));
} else {
offlineMapsETA->setVisible(false);
offlineMapsElapsed->setVisible(false);
}
const int total_files = extractIntFromJson(osmDownloadProgress, "total_files");
const int downloaded_files = extractIntFromJson(osmDownloadProgress, "downloaded_files");
download_failed_state = total_files && osm_download_in_progress && !lastDownloadedTimePoint.has_value() && downloaded_files < total_files;
const auto updateButtonText = processUpdateStatus(pending_update_check, total_files, downloaded_files, osmDownloadProgress, download_failed_state);
osmUpdateBtn->setValue(tr(updateButtonText.c_str()));
osmUpdateBtn->setText(tr(osm_download_in_progress && !download_failed_state ? "Check status" : "Force Update"));
osmDeleteMapsBtn->setValue(formatSize(mapsDirSize));
}
int OsmPanel::extractIntFromJson(const QJsonObject& json, const QString& key) {
return (json.contains(key)) ? json[key].toInt() : 0;
}
std::string OsmPanel::processUpdateStatus(bool pending_update, int total_files, int downloaded_files, const QJsonObject& json, bool failed_state) {
if (pending_update && !osm_download_in_progress && !total_files) {
lastDownloadedTimePoint.reset();
return "Download starting...";
} else if (failed_state) {
return "Error: Invalid download. Retry.";
} else if (osm_download_in_progress && total_files > downloaded_files) {
return formatDownloadStatus(json).toStdString();
} else if (osm_download_in_progress && downloaded_files >= total_files) {
osm_download_in_progress = false;
lastDownloadedTimePoint.reset();
return "Download complete!";
}
if (lastDownloadedTimePoint.has_value()) {
QDateTime dateTime = QDateTime::fromTime_t(std::chrono::system_clock::to_time_t(lastDownloadedTimePoint.value())); //fromMSecsSinceEpoch(duration);
dateTime = dateTime.toLocalTime();
return QString("%1").arg(dateTime.toString("yyyy-MM-dd HH:mm:ss")).toStdString();
}
return "";
}
void OsmPanel::updateMapSize() {
if (mapSizeFuture.has_value() && mapSizeFuture.value().isFinished()) {
mapsDirSize = mapSizeFuture.value().result();
}
if (!mapSizeFuture.has_value() || !mapSizeFuture.value().isRunning()) {
mapSizeFuture = QtConcurrent::run(getDirSize, MAP_PATH);
}
}
+195 -118
View File
@@ -1,9 +1,25 @@
#pragma once
#include "selfdrive/ui/ui.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
#include <deque>
#include <chrono>
#include <optional>
#include <QDir>
#include <QFileInfo>
#include <QtConcurrent/QtConcurrent>
#include "common/swaglog.h"
#include "selfdrive/ui/qt/network/wifi_manager.h"
#include "selfdrive/ui/qt/widgets/controls.h"
#include "selfdrive/ui/qt/offroad/sunnypilot/locations_fetcher.h"
#include "selfdrive/ui/qt/widgets/scrollview.h"
#include "selfdrive/ui/qt/util.h"
#include "selfdrive/ui/ui.h"
#include "system/hardware/hw.h"
constexpr int FAST_REFRESH_INTERVAL = 1000; // ms
constexpr int SLOW_REFRESH_INTERVAL = 5000; // ms
static const QString MAP_PATH = Hardware::PC() ? QDir::homePath() + "/.comma/media/0/osm/offline/" : "/data/media/0/osm/offline/";
class OsmPanel : public QFrame {
Q_OBJECT
@@ -14,129 +30,190 @@ private:
QStackedLayout* main_layout = nullptr;
QWidget* osmScreen = nullptr;
Params params;
Params mem_params{ Hardware::PC() ? "": "/dev/shm/params"};
std::map<std::string, ParamControl*> toggles;
std::optional<QFuture<quint64>> mapSizeFuture;
const SubMaster &sm = *uiState()->sm;
void showEvent(QShowEvent *event) override;
void updateLabels();
bool is_onroad = false;
std::string mapd_version;
bool isWifi() const {return sm["deviceState"].getDeviceState().getNetworkType() == cereal::DeviceState::NetworkType::WIFI; }
bool isMetered() const {return sm["deviceState"].getDeviceState().getNetworkMetered(); }
bool osm_download_in_progress = false;
bool download_failed_state = false;
quint64 mapsDirSize = 0;
QLabel *osmUpdateLbl;
ButtonControl *osmDownloadBtn;
ButtonControl *osmUpdateBtn;
ButtonControl *usStatesBtn;
ButtonControl *osmDeleteMapsBtn;
ButtonControl *setupOsmDeleteMapsButton(QWidget *parent);;
ButtonControl* setupOsmUpdateButton(QWidget *parent);
ButtonControl* setupOsmDownloadButton(QWidget *parent);
ButtonControl* setupUsStatesButton(QWidget *parent);
QTimer *timer;
std::string osm_download_locations;
// void updateButtonControl(ButtonControl *btnControl, QWidget *parent, const QString &initTitle, const QString &allStatesOption);
inline std::vector<std::tuple<QString, QString, QString, QString>> getOsmLocations() {
// location_title, location_name, waypoint, url
std::vector<std::tuple<QString, QString, QString, QString>> locations{
{
"== None ==",
"",
"",
"",
},
{
"Australia",
"australia",
"514911884",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EathcAhPP8dHiTdBiFPIGYoB7zwlUR79OkSTH5dnS-9Shg?e=9cS6er&download=1",
},
{
"Brazil",
"brazil",
"124199196",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EXWcrd8ukahDtoFDXVOxvkYBjGGa9b29fVvRdJlADv3YdA?e=jwabo2&download=1",
},
{
"Canada",
"canada",
"68588664",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETpwTMZftJpCkep2goPmzUQBl5SW_YfHBKLTd7w5CUagkA?e=dshC10&download=1",
},
{
"GCC States",
"gcc-states",
"69021390",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EWtFOVR9mj5CoRpATYR0_tUBWNEcy5jeasp8zO0ZYdduRg?e=Cg0l2Z&download=1",
},
{
"Germany",
"germany",
"461526153",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EbNzQuA8jHBCuGch1WY4cuoBJfK1S6m5odj5qMnOu3HA3Q?e=cNDn30&download=1",
},
{
"Malaysia, Singapore, Brunei",
"malaysia-singapore-brunei",
"1112741782",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQb9bq_YkE1NuFx0jp-52zoBnAbdo4nyLXMnEY1pUbCB8g?e=CoFiUJ&download=1",
},
{
"New Zealand",
"new-zealand",
"154430132",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EVI6eMuNa4dJpXg-r92Vw1EBq5RxR83_7Z5C56AYftg8xQ?e=xouRBl&download=1",
},
{
"South Africa",
"south-africa",
"2729449",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQzbZeus-yBNsgP1KQyLn9wBYZVMMknVoHdB7_ewulLbdA?e=DrnKkb&download=1",
},
{
"Spain",
"spain",
"4263034",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/Ee21EfLqZxxLjksDCB_2dscBXKN9Kch8ZOYz3cIWnVpBTg?e=LWBJ1n&download=1",
},
{
"Taiwan",
"taiwan",
"198637969",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EXgP3Z5_lsBPiQe-aXOOSY4Bu-r54Z_3kKKmT8-IqLO42A?e=tOhUYw&download=1",
},
{
"Turkey",
"turkey",
"698359658",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EWJRrYpV7I1IvOW0LUWrDsUBjEjuKRTla0hA4yRbsVX2xw?e=RGcu8n&download=1",
},
{
"US - Florida",
"florida",
"147221754",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ET9rAvCaretAnO4TjocAddkBFxAziblxblAn9YWzGk5hTw?e=tts5Oz&download=1",
},
{
"US - Midwest",
"us-midwest",
"1059596607",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EaDYnugxokRHhOukMndJuZMBKz-kzPfM6C_-iQcki-r94Q?e=oUX6hz&download=1",
},
{
"US - Northeast",
"us-northeast",
"575213527",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ERrlIKNuT4ZIuUOfr4a3BwABLfd7X1wSq9nUrk9buKrIgA?e=HHIT8R&download=1",
},
{
"US - Pacific",
"us-pacific",
"112909709",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/EQOzoMksk9tAluahQ2qm3rQBHOZD03qFU8Aw4o-TFUTuJA?e=CcBOxL&download=1",
},
{
"US - South",
"us-south",
"243729876",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETizacxh8lFNk8NMVRyKCEsBMMYZl1iq0r-D4IWoOAd_DQ?e=EVIElm&download=1",
},
{
"US - West",
"us-west",
"30023440",
"https://thecorgirosie-my.sharepoint.com/:u:/g/personal/databases_sunnypilot_com/ETTRiSKwlEVKoRHtylruy7wBKgITKXdaCUAkJMuGAN2_mA?e=7jlglL&download=1",
},
};
return locations;
void showEvent(QShowEvent *event) override;
void hideEvent(QHideEvent* event) override;
void updateLabels();
void updateDownloadProgress();
static int extractIntFromJson(const QJsonObject& json, const QString& key);
std::string processUpdateStatus(bool pending_update_check, int total_files, int downloaded_files, const QJsonObject& json, bool failed_state);
ConfirmationDialog* confirmationDialog;
LabelControl *mapdVersion;
LabelControl *offlineMapsStatus;
LabelControl *offlineMapsETA;
LabelControl *offlineMapsElapsed;
std::optional<std::chrono::system_clock::time_point> lastDownloadedTimePoint;
LocationsFetcher locationsFetcher;
void updateMapSize();
bool showConfirmationDialog(QWidget *parent,
const QString &message = QString(),
const QString &confirmButtonText = QString()) const {
const auto _is_metered = isMetered();
const QString warning_message = _is_metered ? tr("\n\nWarning: You are on a metered connection!") : QString();
QString final_message = message.isEmpty() ? tr("This will start the download process and it might take a while to complete.") : message;
final_message += warning_message; // Append the warning message if the connection is metered
const QString final_buttonText = confirmButtonText.isEmpty() ? (_is_metered ? tr("Continue on Metered") : tr("Start Download")) : confirmButtonText;
return ConfirmationDialog::confirm(final_message, final_buttonText, parent);
}
// Refactored methods
std::vector<std::tuple<QString, QString, QString, QString>> getOsmLocations(const std::tuple<QString, QString>& customLocation = defaultLocation) const {
return locationsFetcher.getOsmLocations(customLocation);
}
std::vector<std::tuple<QString, QString, QString, QString>> getUsStatesLocations(const std::tuple<QString, QString>& customLocation = defaultLocation) const {
return locationsFetcher.getUsStatesLocations(customLocation);
}
static QString formatTime(const long timeInSeconds) {
const long minutes = timeInSeconds / 60;
const long seconds = timeInSeconds % 60;
QString formattedTime;
if (minutes > 0) {
formattedTime = QString::number(minutes) + "m ";
}
formattedTime += QString::number(seconds) + "s";
return formattedTime;
}
static QString calculateElapsedTime(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return "Calculating...";
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (downloadedFiles >= totalFiles || totalFiles <= 0) return "Downloaded";
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return "Calculating...";
return formatTime(elapsed);
}
static QString calculateETA(const QJsonObject &jsonData, const std::chrono::system_clock::time_point &startTime) {
using namespace std::chrono;
static steady_clock::time_point lastUpdateTime = steady_clock::now();
static std::deque<double> rateHistory;
constexpr int minDataPoints = 3;
constexpr int historySize = 10;
static QString lastETA = "Calculating ETA...";
if (duration_cast<seconds>(steady_clock::now() - lastUpdateTime).count() < 1) {
return lastETA;
}
if (!jsonData.contains("total_files") || !jsonData.contains("downloaded_files"))
return lastETA;
const int totalFiles = jsonData["total_files"].toInt();
const int downloadedFiles = jsonData["downloaded_files"].toInt();
if (totalFiles <= 0 || downloadedFiles >= totalFiles) {
return totalFiles <= 0 ? "Ready" : "Downloaded";
}
const long elapsed = duration_cast<seconds>(system_clock::now() - startTime).count();
if (elapsed == 0 || downloadedFiles == 0) return lastETA;
const double rate = downloadedFiles / static_cast<double>(elapsed);
if (rateHistory.size() >= historySize) rateHistory.pop_front();
rateHistory.push_back(rate);
if (rateHistory.size() < minDataPoints) return lastETA;
double weightedSum = 0;
for (int i = 0, weight = 1; i < rateHistory.size(); ++i, ++weight) {
weightedSum += rateHistory[i] * weight;
}
const double avgRate = 2 * weightedSum / (rateHistory.size() * (rateHistory.size() + 1));
const long remainingTime = static_cast<long>((totalFiles - downloadedFiles) / avgRate);
if (remainingTime <= 0) return lastETA;
lastETA = formatTime(remainingTime) + " remaining";
lastUpdateTime = steady_clock::now();
return lastETA;
}
static QString formatDownloadStatus(const QJsonObject &json) {
if (!json.contains("total_files") || !json.contains("downloaded_files"))
return "";
const int total_files = json["total_files"].toInt();
const int downloaded_files = json["downloaded_files"].toInt();
if (total_files <= 0) return "Ready";
if (downloaded_files >= total_files) return "Downloaded";
const int percentage = static_cast<int>(100.0 * downloaded_files / total_files);
return QString::asprintf("%d/%d (%d%%)", downloaded_files, total_files, percentage);
}
QString formatSize(quint64 size) const {
if (size == 0 && (!mapSizeFuture.has_value() || mapSizeFuture.value().isRunning())) {
return QString("Calculating...");
}
constexpr qint64 kb = 1024;
constexpr qint64 mb = 1024 * kb;
constexpr qint64 gb = 1024 * mb;
if (size < gb) {
const double sizeMB = size / static_cast<double>(mb);
return QString::number(sizeMB, 'f', 2) + " MB";
} else {
const double sizeGB = size / static_cast<double>(gb);
return QString::number(sizeGB, 'f', 2) + " GB";
}
}
static quint64 getDirSize(QString dirPath) {
quint64 size = 0;
const QString actualDirPath = dirPath.startsWith("~") ? dirPath.replace(0, 1, QDir::homePath()) : dirPath;
QDirIterator it(actualDirPath, QDir::Files | QDir::Dirs | QDir::NoDotAndDotDot | QDir::NoSymLinks, QDirIterator::Subdirectories);
while (it.hasNext()) {
it.next();
if (it.fileInfo().isFile()) {
size += it.fileInfo().size();
}
}
return size;
}
};
@@ -482,6 +482,9 @@ void SunnypilotPanel::updateToggles() {
customTorqueControl->setVisible(false);
}
}
m_tsc->setVisible(false); // TODO: temporarily disable M-TSC until the reimplementation is in place. Remove this line to re-enable the toggle.
m_tsc->setEnabled(false); // TODO: temporarily disable M-TSC until the reimplementation is in place. Remove this line to re-enable the toggle.
}
TorqueFriction::TorqueFriction() : SPOptionControl (
+20 -7
View File
@@ -963,13 +963,10 @@ void AnnotatedCameraWidget::drawHud(QPainter &p) {
// Bottom bar road name
if (showDebugUI && !roadName.isEmpty()) {
const int h = 38;
QRect bar_rc(rect().left(), rect().top(), rect().width(), h);
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 100));
p.drawRect(bar_rc);
p.setFont(InterFont(28, QFont::Bold));
drawCenteredText(p, bar_rc.center().x(), bar_rc.center().y(), roadName, QColor(255, 255, 255, 200));
int font_size = splitPanelVisible ? 38 : 50;
int h = splitPanelVisible ? 18 : 26;
p.setFont(InterFont(font_size, QFont::Bold));
drawRoadNameText(p, rect().center().x(), h, roadName, QColor(255, 255, 255, 255));
}
// Turn Speed Sign
@@ -1016,6 +1013,22 @@ void AnnotatedCameraWidget::drawCenteredText(QPainter &p, int x, int y, const QS
p.drawText(real_rect, Qt::AlignCenter, text);
}
void AnnotatedCameraWidget::drawRoadNameText(QPainter &p, int x, int y, const QString &text, QColor color) {
QRect real_rect = p.fontMetrics().boundingRect(text);
real_rect.moveCenter({x, y});
QRect real_rect_adjusted(real_rect);
real_rect_adjusted.adjust(-UI_ROAD_NAME_MARGIN_X, 5, UI_ROAD_NAME_MARGIN_X, 0);
QPainterPath path;
path.addRoundedRect(real_rect_adjusted, 10, 10);
p.setPen(Qt::NoPen);
p.setBrush(QColor(0, 0, 0, 100));
p.drawPath(path);
p.setPen(color);
p.drawText(real_rect, Qt::AlignCenter, text);
}
void AnnotatedCameraWidget::drawVisionTurnControllerUI(QPainter &p, int x, int y, int size, const QColor &color,
const QString &vision_speed, int alpha) {
QRect rvtc(x, y, size, size);
+1
View File
@@ -127,6 +127,7 @@ private:
void speedLimitSignPulse(int frame);
void speedLimitWarning(QPainter &p, QRect sign_rect, const int sign_margin);
void mousePressEvent(QMouseEvent* e) override;
void drawRoadNameText(QPainter &p, int x, int y, const QString &text, QColor color);
Params params;
QVBoxLayout *main_layout;
+1
View File
@@ -109,6 +109,7 @@ public:
ButtonControl(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr);
inline void setText(const QString &text) { btn.setText(text); }
inline QString text() const { return btn.text(); }
inline void click() { btn.click(); }
signals:
void clicked();
+2
View File
@@ -20,6 +20,8 @@
const int UI_BORDER_SIZE = 30;
const int UI_HEADER_HEIGHT = 420;
const int UI_ROAD_NAME_MARGIN_X = 14;
struct FeatureStatusText {
const QStringList dlp_list_text = { "Laneful", "Laneless", "Auto" };
const QStringList gac_list_text = { "Maniac", "Aggressive", "Standard", "Relaxed" };
+2
View File
@@ -0,0 +1,2 @@
# MAPD implementation by pfeiferj
https://github.com/pfeiferj/openpilot-mapd/releases/
BIN
View File
Binary file not shown.