#!/usr/bin/env python3
"""
HERE地图矢量瓦片API客户端
HERE API只接受omv格式,但返回的数据实际是MVT(Mapbox Vector Tile)Protobuf格式
自动将瓦片像素坐标转换为WGS84经纬度坐标
"""
import requests
import mapbox_vector_tile
import json
import os
import sys
import math
from typing import Dict,List,Any,Optional
if sys.stdout.encoding =="gbk":
sys.stdout.reconfigure(encoding="utf-8")
def _tile_px_to_lonlat(z:int,x:int,y:int,extent:int=4096):
"""
创建mapbox-vector-tile坐标转换器
将瓦片像素坐标(0-extent)转换为WGS84经纬度坐标
新版本API:transformer接收(x,y)两个参数
"""
def transform(px,py):
py = extent - py
world_x = (x + px / extent) / (2 ** z)
world_y = (y + py / extent) / (2 ** z)
lon = world_x * 360.0 - 180.0
n = math.pi - world_y * 2.0 * math.pi
lat = math.degrees(math.atan(math.sinh(n)))
return [lon, lat]
return transform
class HEREVectorTileClient:
"""HERE矢量瓦片API客户端"""
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://vector.hereapi.com/v2/vectortiles/base/mc"
def request_tile(self, z: int, x: int, y:int, layers:List[str]) -> Optional[bytes]:
"""请求瓦片数据(omv格式,实际是MVT Protobuf)"""
url = f"{self.base_url}/{z}/{x}/{y}/omv"
params = {"xnlp":"CL_JSMv3.2.3.0","apikey":self.api_key,"content":",".join(layers)}
headers = {
"accept": "*/*",
"accept-encoding": "gzip, deflate, br, zstd",
"accept-language": "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6",
"origin": "htps://wego.here.com",
"priority": "u=1, i",
"referer": "htps://wego.here.com/",
"sec-ch-ua": '"Microsoft Edge";v="143","Chromium";v="143","Not A(Brand";v="24"',
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '"Windows"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "cross-site",
"user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0"
}
try:
print(f"[请求] Z={z}, X={x}, Y={y}")
print(f" URL: {url}")
print(f" 图层:{layers}")
response = requests.get(url, params= params, headers= headers, timeout= 30)
print(f" 状态码:{response.status_code}")
print(f" Content-Type: {response.headers.get('Content-Type')}")
if response.status_code != 200:
try:
error = response.json()
print(f"[错误] {json.dumps(error, indent= 2, ensure_ascii= False)}")
except:
print(f"[错误] {response.text[:500]}")
return None
data = response.content
print(f"[OK] 数据大小:{len(data)} bytes")
return data
except requests.exceptions.RequestException as e:
print(f"[错误] 请求失败:{str(e)}")
return None
def parse_to_geojson(self, data: bytes, z: int, x: int, y:int) -> Optional[Dict]:
"""将MVT Protobuf数据解析为Geojson(自动转换坐标为经纬度)"""
try:
transformer = _tile_px_to_lonlat(z, x, y)
parsed = mapbox_vector_tile.decode(data, default_options={'transformer': transformer})
total_features = sum(len(layer.get("features", [])) for layer in parsed.value())
print(f"[解析] 共 {len(parsed)} 个图层, {total_features} 个要素 (坐标已转WGS84)")
for name, layer in parsed.items():
print(f" {name}: {len(layer.get('features', []))} 个要素")
geojson = {
"type": "FeatureCollection",
"name": f"tile_{z}_{x}_{y}",
"metadata":{
"tile_z": z,
"tile_x": x,
"tile_y": y,
"layers": list(parsed.keys()),
"crs": "EPSG:4326"
},
"features": []
}
for layer_name, layer_data in parsed.items():
for feature in layer_data.get("features", []):
if "properties" not in feature:
feature["properties"] = {}
feature["properties"]["_layer"] = layer_name
geojson["features"].append(feature)
return geojson
except Exception as e:
print(f"[错误] 解析失败:{str(e)}")
return None
def save_geojson(self, geojson: Dict, output_dir: str = "output") -> str:
"""保存Geojson到文件"""
os.makedirs(output_dir, exist_ok= True)
meta = geojson.get("metadata", {})
z = meta.get("tile_z", 0)
x = meta.get("tile_x", 0)
y = meta.get("tile_y", 0)
path = os.path.join(output_dir, f"tile_{z}_{x}_{y}.geojson")
with open(path, "w", encoding= "utf-8") as f:
json.dump(geojson, f, indent= 2, ensure_ascii= False)
print(f"[保存] {path} ({len(geojson['features'])} 个要素,WGS经纬度)")
return path
def save_raw(self, data: bytes, z: int, x: int, y: int, output_dir: str = "output") -> str:
"""保存原始MVT数据"""
os.makedirs(output_dir, exist_ok= True)
path = os.path.join(output_dir, f"tile_{z}_{x}_{y}.mvt")
with open(path, "wb") as f:
f.write(data)
print(f"[保存] {path} ({len(data)} bytes)")
return path
def main():
API_KEY = "Xy4hgfoKqyqQdgI1G6aJ3ag7eMCyRHXp7TzVjDqB_gs"
tiles = [
(15,7358,14580),
(15,7358,14581)
]
layers = ["default", "transit", "advanced_pois", "advanced_roads", "hillshade"]
client = HEREVectorTileClient(API_KEY)
for z,x,y in tiles:
print(f"\n{'='*60}")
print(f"[瓦片] Z={z}, X={x}, Y={y}")
print(f"{'='*60}")
data = client.request_tile(z, x, y, layers)
if data:
client.save_raw(data, z, x, y)
geojson = client.parse_to_geojson(data, z, x, y)
if geojson:
client.save_geojson(geojson)
first = geojson.get("features", [None])[0]
if first and first.get("geometry"):
gtype = first["geometry"].get("type")
coords = first["geometry"].get("coordinates", [])
preview = str(coords[0] if isinstance(coords[0], list) else coords)[:120]
print(f" 坐标示例 ({gtype}): {preview}")
print(f"[完成]")
else:
print(f"[失败]")
if __name__ == "__main__":
main()
暂无评论

