diff --git a/README.md b/README.md index 61912cc..7914f3b 100644 --- a/README.md +++ b/README.md @@ -183,8 +183,8 @@ Services covering multiple countries are on the left; services covering one spec ✔ ✔7 - ✔7 - ✔
+ ✔11 + ✔ ✔7 @@ -196,7 +196,7 @@ Services covering multiple countries are on the left; services covering one spec ✔ ⚫ - ✔ + ⚫12 ✔ ⚫ @@ -276,4 +276,6 @@ Services covering multiple countries are on the left; services covering one spec 7: Only heading; pitch/roll do not appear to be available 8: Previous and next image in sequence 9: Month and year only -10: There is a `has_depth` field in the raw metadata, but I've yet to find a panorama that actually has depth. \ No newline at end of file +10: There is a `has_depth` field in the raw metadata, but I've yet to find a panorama that actually has depth +11: Pitch/roll are only available for the new 3D imagery +12: Camera altitude is available, however \ No newline at end of file diff --git a/streetlevel/naver/api.py b/streetlevel/naver/api.py index 8a4b72c..9fcdd69 100644 --- a/streetlevel/naver/api.py +++ b/streetlevel/naver/api.py @@ -11,15 +11,15 @@ def build_find_panorama_request_url(lat: float, lon: float) -> str: def build_find_panorama_by_id_request_url(panoid: str, language: str) -> str: - return f"https://panorama.map.naver.com/metadata/basic/{panoid}?lang={language}&version=2.1.0" + return f"https://panorama.map.naver.com/metadataV3/basic/{panoid}?lang={language}" -def build_timeline_request_url(panoid: str) -> str: - return f"https://panorama.map.naver.com/metadata/timeline/{panoid}" +def build_timeline_request_url(timeline_id: str) -> str: + return f"https://panorama.map.naver.com/metadata/timeline/{timeline_id}" def build_around_request_url(panoid: str) -> str: - return f"https://panorama.map.naver.com/metadata/around/{panoid}?lang=ko" + return f"https://panorama.map.naver.com/metadataV3/around/{panoid}?lang=ko" def build_depth_request_url(panoid: str) -> str: diff --git a/streetlevel/naver/panorama.py b/streetlevel/naver/panorama.py index 116f6b8..f38259b 100644 --- a/streetlevel/naver/panorama.py +++ b/streetlevel/naver/panorama.py @@ -3,7 +3,7 @@ from dataclasses import dataclass from datetime import datetime from enum import IntEnum -from typing import List +from typing import List, Optional import numpy as np @@ -27,7 +27,11 @@ class PanoramaType(IntEnum): INDOOR_HIGH = 11 #: UNDERWATER = 12 #: TREKKER = 13 #: - MESH_EQUIRECT = 15 #: + MESH_EQUIRECT = 15 + """ + An Apple-like panorama which can be fetched in equirectangular projection + and for which a 3D mesh is available. + """ INDOOR_3D = 100 #: @@ -49,10 +53,18 @@ class NaverPanorama: heading: float = None """Heading in radians, where 0° is north, 90° is east, 180° is south and 270° is west.""" - elevation: float = None - """Elevation at the capture location in meters.""" - camera_height: float = None - """Height of the camera in meters above ground.""" + altitude: float = None + """Altitude of the camera above sea level, in meters.""" + pitch: float = None + """ + Pitch offset for upright correction of the panorama, in radians. + Only available if ``has_equirect`` is True; the field is set to 0 otherwise. + """ + roll: float = None + """ + Roll offset for upright correction of the panorama, in radians. + Only available if ``has_equirect`` is True; the field is set to 0 otherwise. + """ max_zoom: int = None """Highest zoom level available for this panorama.""" @@ -60,7 +72,10 @@ class NaverPanorama: neighbors: Neighbors = None """A list of nearby panoramas.""" links: List[Link] = None - """The panoramas which the white dots in the client link to.""" + """ + The panoramas which the white dots in the pre-3D client link to. + This appears to be unused in the new client. + """ historical: List[NaverPanorama] = None """A list of panoramas with a different date at the same location. Only available if the panorama was retrieved by ``find_panorama_by_id``.""" @@ -80,11 +95,16 @@ class NaverPanorama: """The title field, which typically contains the street name.""" depth: np.ndarray = None - """The depth maps of the faces.""" + """The legacy depth maps of the cubemap faces.""" + has_equirect: bool = None + """ + If True, this panorama can be fetched as either in equirectangular projection or as a cubemap. + If False, only a cubemap is available. + """ panorama_type: PanoramaType = None """The panorama type. Most identifiers are taken directly from the source.""" - overlay: Overlay = None + overlay: Optional[Overlay] = None """ Curiously, in pre-3D imagery, Naver masks their car twice: once with an image of a car baked into the panorama, and additionally with an image of the road beneath it (like Google and Apple), which is served as a separate file @@ -121,7 +141,6 @@ def __str__(self): @dataclass class Overlay: """URLs to the images from which the overlay hiding the mapping car is created.""" - source: str """URL to the texture.""" mask: str @@ -131,7 +150,6 @@ class Overlay: @dataclass class Neighbors: """Nearby panoramas.""" - street: List[NaverPanorama] """Nearby panoramas taken at street level.""" other: List[NaverPanorama] diff --git a/streetlevel/naver/parse.py b/streetlevel/naver/parse.py index ce3ceae..e0e9fc4 100644 --- a/streetlevel/naver/parse.py +++ b/streetlevel/naver/parse.py @@ -3,58 +3,74 @@ from typing import List, Optional from streetlevel.dataclasses import Link +from streetlevel.geo import get_bearing from streetlevel.naver.panorama import Neighbors, NaverPanorama, PanoramaType, Overlay def parse_panorama(response: dict) -> NaverPanorama: - basic = response["basic"] - elevation = basic["land_altitude"] * 0.01 + altitude = response["altitude"] + if response["camera_angle"][0] != 0: + pitch = math.radians(90 - response["camera_angle"][0]) + roll = math.radians(90 - response["camera_angle"][2]) + else: + pitch = 0 + roll = 0 pano = NaverPanorama( - id=basic["id"], - lat=basic["latitude"], - lon=basic["longitude"], - heading=math.radians(basic["camera_angle"][1]), - max_zoom=int(basic["image"]["segment"]) // 2, - timeline_id=basic["timeline_id"], - date=_parse_date(basic["photodate"]), - is_latest=basic["latest"], - description=basic["description"], - title=basic["title"], - panorama_type=PanoramaType(int(basic["dtl_type"])), - elevation=elevation, - camera_height=(basic["camera_altitude"] * 0.01) - elevation + id=response["id"], + lat=response["latitude"], + lon=response["longitude"], + heading=math.radians(response["camera_angle"][1]), + pitch=pitch, + roll=roll, + max_zoom=int(response["segment"]) // 2, + timeline_id=response["info"]["timeline_id"], + date=_parse_date(response["info"]["photodate"]), + is_latest=response["info"]["latest"], + description=response["info"]["description"], + title=response["info"]["title"], + panorama_type=PanoramaType(int(response["dtl_type"])), + altitude=altitude, + has_equirect=response["proj_type"] == "equirect" ) - if len(basic["image"]["overlays"]) > 1: + if response["overlay_type"] == "car": pano.overlay = Overlay( - "https://panorama.map.naver.com" + basic["image"]["overlays"][1][0], - "https://panorama.map.naver.com" + basic["image"]["overlays"][1][1]) + f"https://panorama.map.naver.com/api/v2/overlays/floor/{pano.id}", + f"https://panorama.map.naver.com/resources/style/mask.png" + ) - pano.links = _parse_links(basic["links"]) + pano.links = _parse_links(response["links"], pano.lat, pano.lon) return pano def parse_neighbors(response: dict, parent_id: str) -> Neighbors: - street = _parse_neighbor_section(response, "street", parent_id) - other = _parse_neighbor_section(response, "air", parent_id) + if "street" in response["panoramas"]: + street = _parse_neighbor_section(response["panoramas"]["street"], parent_id) + else: + street = None + + if "air" in response["panoramas"]: + other = _parse_neighbor_section(response["panoramas"]["air"], parent_id) + else: + other = None + return Neighbors(street, other) -def _parse_neighbor_section(response: dict, section: str, parent_id: str) -> List[NaverPanorama]: +def _parse_neighbor_section(section: dict, parent_id: str) -> List[NaverPanorama]: panos = [] - if section in response["around"]["panoramas"]: - for raw_pano in response["around"]["panoramas"][section][1:]: - if raw_pano[0] == parent_id: - continue - elevation = raw_pano[4] * 0.01 - pano = NaverPanorama( - id=raw_pano[0], - lat=raw_pano[2], - lon=raw_pano[1], - elevation=elevation, - camera_height=(raw_pano[3] * 0.01) - elevation) - panos.append(pano) + for raw_pano in section: + if raw_pano["id"] == parent_id: + continue + pano = NaverPanorama( + id=raw_pano["id"], + lat=raw_pano["latitude"], + lon=raw_pano["longitude"], + altitude=raw_pano["altitude"], + panorama_type=PanoramaType(int(raw_pano["dtl_type"])), + ) + panos.append(pano) return panos @@ -80,7 +96,7 @@ def parse_nearby(response: dict) -> NaverPanorama: date=_parse_date(feature["properties"]["photodate"]), description=feature["properties"]["description"], title=feature["properties"]["title"], - elevation=elevation, + altitude=elevation, camera_height=(feature["properties"]["camera_altitude"] * 0.01) - elevation, panorama_type=PanoramaType(int(feature["properties"]["type"])), ) @@ -90,18 +106,19 @@ def _parse_date(date_str: str) -> datetime: return datetime.strptime(date_str, "%Y-%m-%d %H:%M:%S") -def _parse_links(links_json: List) -> Optional[List[Link]]: +def _parse_links(links_json: List, pano_lat: float, pano_lon: float) -> Optional[List[Link]]: if len(links_json) < 2: return None links = [] - for linked_json in links_json[1:]: - linked = NaverPanorama( - id=linked_json[0], - title=linked_json[1], - lat=linked_json[5], - lon=linked_json[4], + for link_json in links_json: + link = NaverPanorama( + id=link_json["id"], + lat=link_json["latitude"], + lon=link_json["longitude"], + panorama_type=PanoramaType(int(link_json["dtl_type"])), + altitude=link_json["altitude"] ) - angle = math.radians(float(linked_json[2])) - links.append(Link(linked, angle)) + angle = get_bearing(pano_lat, pano_lon, link.lat, link.lon) + links.append(Link(link, angle)) return links