QQ/Soso/Tencent Street View
Date: 2024-12-18
Tencent has a street view service in their mapping app, also known as Soso Maps/QQ Map. The Chinese GeoGuessr community on tuxun.fun figured out how to access this service, and I'd like to share what they learned in English :)
I'll mostly use the name Soso Maps in this post because that's what the tuxun players appear to call it, even though I think it's an old name(?).
This post is a work in progress! I'm adding things here as I figure them out. I'll organise it a bit afterwards.
Background
Soso Maps is available in a web browser, at https://map.qq.com, but its street view service is not. You can only get it through the mobile app. You can download the mobile app by scanning the QR code on the website.
The panoramas appear to be from 2013 through 2016. Unfortunately, it's all a bit outdated. That's probably why they haven't made an effort to make it accessible on the website again after their Flash-based viewer stopped working!
The quality of the panoramas is quite good, however. The resolution is not amazing, but the colours are natural, similar to some of Google's Street View coverage. With many other street view services, poor colours and white balance are a problem. For example, Baidu's street view is often too dark or too bright.
Both Baidu and Soso Maps mostly cover cities. There's not many rural roads, but even many small cities are covered.
Coordinates
Soso Maps uses two coordinate systems: the standard Chinese GCJ-02 system, and an apparently custom system. Depending on where you get your coordinates from, you may have to convert them first.
If you get your coordinates from a Google Map, you actually don't need to convert at all: Google's road map in China is based on GCJ-02 coordinates, but is rendered as if the coordinates were typical WGS84 coordinates. That's why the mapped roads don't line up with the satellite map and even spill outside China's borders sometimes.
Limited use of GCJ-02 is still nice compared to alternatives--as far as I can tell, Baidu exclusively uses a proprietary coordinate system that has further obfuscation on top of GCJ-02.
In this post I'll use lng
and lat
for GCJ-02 coordinates, and x
and y
for the custom system (…Soso coordinates?).
Conversion between GCJ-02 and Soso coordinates works like this:
type TencentCoord = [x: number, y: number]
type Gcj02Coord = [lng: number, lat: number]
/** Unknown constant */
const A = 114.59155902616465
/** World size for tencent coordinates */
const SCALE = 111319.49077777778
function deg2rad (deg: number) {
return deg * Math.PI / 180
}
function rad2deg (rad: number) {
return rad * 180 / Math.PI
}
function tencentToGcj02([x, y]: TencentCoord): Gcj02Coord {
return [
x / SCALE,
A * Math.atan(Math.exp(deg2rad(y / SCALE))) - 90,
]
}
function gcj02ToTencent([lng, lat]: Gcj02Coord): TencentCoord {
return [
lng * SCALE,
rad2deg(SCALE * Math.log(Math.tan((lat + 90) / A))),
]
}
The lng
/x
transformation is straightforward. I'm not entirely sure what the lat
/y
transformation is doing, but I imagine it's to handle distortion in the mercator projection as you move away from the equator.
Pano IDs
Each panorama has an svid
. An example is 10241052150620104415000
.
Several parts are meaningful:
1024 1052 15 06 20 10 44 15000
City YY-MM-DD HH:mm
Tencent assigns IDs to covered cities. The IDs are stored in a streetcfg.dat
file that comes with the mobile app.
To parse out the coverage date from the pano ID, you can use:
const date = new Date(
Number(panoId.slice(8, 10)),
Number(panoId.slice(10, 12)) - 1,
Number(panoId.slice(12, 14)),
Number(panoId.slice(14, 16)),
Number(panoId.slice(16, 18)),
}
Searching for panoramas
You can search for panoramas using the HTTP endpoint:
https://sv.map.qq.com/xf?lat={lat}&lng={lng}&r={radius}&output=json
{lat}
and {lng}
are the GCJ-02 coordinates to centre the search on, and {radius}
is a distance in metres.
The response is an object like the below:
{
"info": {
"errno": 0,
"type": 1,
"error": 0
},
"detail": {
"svid": "10241052150620104415000",
"x": 12213292.98,
"y": 2266415.95,
"zoom": 1,
"pitch": 0,
"heading": 180,
"src": "1",
"road_name": "NA",
"get_way": 1,
"url": "",
"type": 0,
"group_name": ""
}
}
I'm not sure what the info
object means. The errno
and error
properties do not appear to actually be populated, even if you provide incorrect parameters, or if there are no results.
The detail
object has some of what we want: its svid
property contains the resulting panorama ID, if any. The x
and y
properties are the Soso coordinates for the panorama.
The remaining properties appear to always have a default value.
So really, the main thing here is the svid
.
const response = await fetch(
`https://sv.map.qq.com/xf?lat=${lat}&lng=${lng}&r=${radius}`
)
const json = await response.json()
// The `|| null` makes sure we get null if svid is present,
// but is the empty string.
const svid = json.detail.svid || null
We can use this to request more information.
Panorama metadata
Once you have a panorama ID, you can look up some metadata about the panorama.
https://sv.map.qq.com/sv?output=json&svid={pano_id}
The response shape is a JSON object. This is its shape as a TypeScript type:
interface TencentMetadataResponse {
info: {
errno: number,
type: number,
error: number,
},
detail: {
addr: {
x_lng: number, // GCJ-02
y_lat: number, // GCJ-02
}
// Lots of nearby panoramas
all_scenes: {
x: number,
y: number,
svid: string,
dy: number,
}[]
basic: {
svid: string,
rdid: string,
sv_type: number,
rd_id: string,
rd_ver: number,
rd_dis: number,
x: number,
y: number,
h: number,
orix: number,
oriy: number,
mode: 'day', // Or probably other strings.
forward_step: number,
sno: string,
append_addr: string,
source: 'soso',
dir: string,
level0: `${number}*${number}`,
level1: `${number}*${number}`,
mlevel0: `${number}*${number}`,
mlevel1: `${number}*${number}`,
mclevel0: `${number}*${number}`,
mclevel1: `${number}*${number}`,
tile_width: string,
mtile_width: string,
mctile_width: string,
tile_height: string,
mtile_height: string,
mctile_height: string,
proj_type: string,
mproj_type: string,
mcproj_type: string,
ln: string,
mln: string,
mcln: string,
dln: string,
dlevel0: `${number}*${number}`,
dtile_width: string,
dtile_height: string,
trans_svid: string,
quit_svid: string,
quit_name: string,
scenic_id: string,
scenic_name: string,
best_dir: string,
ad_code: string,
name: string,
description: string,
best_pitch: string,
zoom: string,
poi_id: string,
pitch_min: string,
pitch_max: string,
}
history: {
nodes: {
default: number,
svid: string,
}[],
},
markers: unknown[],
region: {
entrances: unknown[],
},
roads: {
id: string,
valid: 0 | 1,
name: string,
width: number,
points: {
x: number,
y: number,
svid: string,
order: number,
}[],
}[],
vpoints: {
x: number,
y: number,
id: number,
svid: string,
rdid: string,
link: {
x: number,
y: number,
svid: string,
rdid: string,
}[],
}[],
},
}
The history
field only contains a panorama ID, but we can parse out the date as shown above.
The x
/y
fields are all Soso Maps coordinates.
Rendering a panorama
Panoramas are available in an equirectangular projection, at 4K and 8K resolution, in tiles of 512×512px.
In theory, different panoramas could have different tile configurations described by the /sv
endpoint response, but in practice it seems like it's always the same.
https://sv{server}.map.qq.com/tile?from=web&svid={svid}&level={z}&x={x}&y={y}
server
is an integer between 1 and 4 (inclusive).svid
selects the panorama.z
has two levels: 0 for 8×4 tiles, and 1 for 16×8 tiles.x
andy
specify the tile coordinate.
Unlike most other services, there is not a full set of zoom levels. Typically, for equirectangular viewers with multiple zoom levels, you'd get a single tile containing the entire panorama at zoom level 0, 2×1 tiles at zoom level 1, 4×2 tiles at zoom level 2, etc. Soso Maps only provides the tiles for what would normally be zoom level 3 and 4.
Tuxun appears to stitch the other levels together on their own server, but it requires downloading multiple tiles and downscaling them for each output tile, so it is a little slow.
For the zoom levels 1 and 2, I don't think there is a way around that. But for the single-tile zoom level 0, we can actually use the thumbnail endpoint that Soso Maps provides.
https://sv{server}.map.qq.com/thumb?x=0&y=0&from=html5&level=0&svid={svid}
Filling in the parameters, we get a 512×256 pixel tile:
Coverage lines
It's hard to use the coverage well when you don't know where it is. That's why most services provide lines on a map indicating where coverage is.
Soso Maps does do this, but it's very unusual and very complicated! You can read about it in the next post.