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