Unlocking Google Street View: Extracting Panorama ID and Location from the Google Maps Link

Ever wondered how Google works its magic with Maps? 🗺️ They've got these cool Maps URLs that hold location secrets, like where to find panoramas and other goodies. 🌍🌆

I got curious during my little pet project and needed some of that location jazz and panorama ID pizzazz. But guess what? Google keeps the recipe for those URLs hush-hush. 🤫 So, I put on my virtual Sherlock Holmes hat 🕵️ and went on an adventure to reverse-engineer the map-magic. 🚀 Now, I'm here to share the treasure map with you! X marks the spot! 📍🗝️ 

Use cases

User shares a location or panorama from the Google Maps app or web-site to our app. As a result, a shortened link is received. Due to Google's redirection from the shortened URL to the full URL, we should send an HTTP request to the shortened link to receive the full URL. This URL will be parsed to gain useful information.

There are several cases of shared locations:

  1. Point
  2. Place
  3. Panorama
  4. User panorama
  5. Search for a location, place or address
  6. Special case #1: Place + Map moved
  7. Special case #2: Place + Panorama

Other sections:

  1. Web-API: Panorama preview
  2. Web-API: Panorama metadata
  3. Web-API: Place information
  4. Web-API: Sign your request
  5. Android Place API
  6. Android Geocoder
  7. CID issue
  8. Conclusion

1. Point

Web-browser shared link:

goo.gl/maps/Up94VLPN5kPJ74WEA

Unshortened link:

www.google.com/maps/search/48.718116,+2.627758?entry=tts&shorturl=1

I use domain ".com" in all my examples, but globally it can be any of supported ones: supported_domains.

Query segment may be discarded:

www.google.com/maps/search/48.718116,+2.627758

Plus sign is an URL encoded space sign.

So, we can get the location of the place: (lat: 48.718116, lon: 2.627758). Nevertheless, I assume that there are cases when value of this attribute contains not coordinates, but rather an address or a description of the place.

The link from the mobile app has a different domain:

maps.app.goo.gl/7qWFiAfvRsBEKTz56

is redirected to:

www.google.com/maps/place/ -13.165331,-72.548991/ data = !4m6 !3m5 !1s0 !7e2 !8m2 !3d -13.1653311 !4d -72.5489906

Сoordinates are also provided as path attributes, and additionally, they are present in the data path attribute.

The "data" segment consists of sections, each starting with an exclamation mark "!", followed by a digit, a letter, and the data. Let's describe some of them:

  • !4m6 - "m" stands for a container, first "4" stands for "container of containers", first "3" - "container of values", last "6" is the number of included items
  • !8m2 - coordinates container, two children included
  • !3d - latitude, double value
  • !4d - longitude, double value

A point can be represented as a place with coordinates in DMS format, serving as a name:

      
www.google.com/maps/place/ 48°43'05.4"N+2°37'40.4"E/ @48.7181667,2.6278889,17z/ data = !3m1 !4b1 !4m4 !3m3 !8m2 !3d 48.7181667 !4d 2.6278889
  • 17z - stands for "zoom level" = 17

2. Place

Web-link:

www.google.com/maps/place/ Buckingham+Palace/ @51.5016211,-0.1428878,17z/ data = !3m1 !5s 0x487605276d38fb6b:0xe1c60228d7946675 !4m6 !3m5 !1s 0x48760520cd5b5eb5:0xa26abf514d902a7 !8m2 !3d 51.5016211 !4d -0.1428878 !16z L20vMDE5OGc !5m1 !1e4

App-link:

www.google.com/maps/place/ Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/ data = !4m2 !3m1 !1s 0x48760520cd5b5eb5:0xa26abf514d902a7

The useful information is limited just to the place name.

3. Panorama

Web-link:

www.google.com/maps/ @48.8583445,2.2943296,2a,75y,313.07h,81.5t/ data = !3m7 !1e1 !3m5 !1s _BgQhZS8mY-Zix5BdCt2mA !2e0 !6s https://streetviewpixels-pa.googleapis.com/v1/thumbnail ? panoid = _BgQhZS8mY-Zix5BdCt2mA & cb_client = maps_sv.tactile.gps & w = 203 & h = 100 & yaw = 258.24527 & pitch = 0 & thumbfov = 100 !7i 13312 !8i 6656 !5m1 !1e4
  • 1e1 - is an indicator that data contains panorama info.

The panorama ID is always located in the "!1s" field and followed either with "!2e0" or "!2e10", where "0" indicates that the panorama belongs to Google and "10" - to the private person (user). Moreover, for user panoramas, this string won't exactly be the panorama ID, but we'll get to that later.

So, the panorama ID can be preceded by such a set of characters:

  • !3m7!1e1!3m5!1s
  • !3m6!1e1!3m4!1s
  • !3m4!1e1!3m2!1s

@-path attribute contains camera settings:

  • 48.8583445,2.2943296 - coordinates (latitude, longitude)
  • 75y - vertical view angle, limited: [15, 90]
  • 313.07h - heading / bearing / azimuth
  • 81.5t - tilt / pitch

IMPORTANT: all slashes in data section must be encoded as "%2F".

App-link:

www.google.com/maps/ @48.858345,2.2943299,0a,75y,312.57h,84.6t/ data = !3m4 !1e1 !3m2 !1s _BgQhZS8mY-Zix5BdCt2mA !2e0

4. User panorama

Web-link:

www.google.com/maps/ @50.45007,30.5238129,3a,48.9y,228.12h,97.74t/ data = !3m6 !1e1 !3m4 !1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5 !2e10 !7i 12082 !8i 6041

So, here we can see the "!2e10" ending after the panorama ID section, and it indicates that this is a user panorama. The issue is that the panorama ID in such URLs is not the ID of the actual panorama; it's more like a "creator panorama ID" which can't be used to obtain this panorama by API. I haven't been able to figure out how to translate it into the actual panorama ID.

The only option we have is to use the shooting location's coordinates and hope that Google consistently finds the desired panorama. My expectation is based on the fact that user-generated panoramas are individual and occupy specific spots with some accuracy on the map. Furthermore, these custom panoramas lack the "history stack" feature ("history stack" allows you to select a panorama based on its capture date for a particular location). Nevertheless, if I come across a relevant case, then I will find an alternative way to obtain the correct panorama.

App-link almost the same:

www.google.com/maps/ @50.4500732,30.5238113,0a,75y,231.11h,93.48t/ data = !3m4 !1e1 !3m2 !1s AF1QipPhTjCOEXu7rYyTQSOnaYLzu1vyUUqumgW8XKY5 !2e10

But there is one nuance: not all user panoramas that open in the browser can be accessed through the API. This situation arises because user panoramas are associated with the Google Photos service, and in order for a panorama to be opened through the Street View Panorama API, it must have an identifier in the Google Street View service. Unfortunately, this is not always the case. To check if a panorama is registered in the Google Street View service, you can look for a blue dot at the location of the panorama. If it's not there, then it will be impossible to find this panorama through the Google Street View API.

For example, there is a registered panorama, blue dot is under the Pegman: 

Chernihiv. Hotel "Ukraine" after being hit by a ruZZian missile.

  • photo ID: AF1QipN9GteezKnVzb23gMzWsirXHLDqeTreSvuQzIsc
  • panorama ID: CAoSLEFGMVFpcE45R3RlZXpLblZ6YjIzZ016V3NpclhITERxZVRyZVN2dVF6SXNj
  • location: 51.494505427553,31.294799751616

There is a not registered panorama, no blue dot under the Pegman:

  • photo ID: AF1QipOi38JA6bFk_E9ERR7kNDPkMt6VMIxvjQHkpGSv
  • location: 51.5250583,31.3265528

5. Search for a location, place or address

These links are generated in the mobile app only, but not for all search results; usually, they are place-links.

  • Location
www.google.com/maps?q=50.813588,-2.475488
  • Place
www.google.com/maps?q=London,+Great+Britain
  • Address
www.google.com/maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA

6. Special case #1: Place + Map moved

  1. Click on any place-marker.
  2. Move map camera away.
  3. Click on Share button on the Marker info window.

Example:

  • Place: Empire State Building
  • Map camera: East River
www.google.com/maps/place/ Empire+State+Building/ @40.7321378,-73.9691401,17.87z/ data = !3m1 !5s 0x8b398fecd1aea119:0x76fa1e3ac5a94c70 !4m6 !3m5 !1s 0x89c259a9b3117469:0xd134e199a405a163 !8m2 !3d 40.7484405 !4d -73.9856644 !16z L20vMDJuZF8
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7321378,-73.9691401 = 40°43'55.7"N, 73°58'08.9"W - East River
  • data-location:
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building

Result:

  • place = marker place name
  • @location = map camera location
  • data-location = place marker location

7. Special case #2: Place + Panorama

  1. Select any place marker.
  2. Turn on Street View layer.
  3. Move map camera away.
  4. Open panorama on any place, not the original place.
  5. Click Share on panorama view (under triple dots menu on top right corner).

Example:

  • Place: Empire State Building
  • Panorama: Flatiron Building
www.google.com/maps/place/ Empire+State+Building/ @40.7413886,-73.9891371,3a,75y,230.15h,116.97t/ data = !3m6 !1e1 !3m4 !1s nhbQjZVsxbtfxb9rTUhZmQ !2e0 !7i 16384 !8i 8192 !4m6 !3m5 !1s 0x89c259a9b3117469:0xd134e199a405a163 !8m2 !3d 40.7484405 !4d -73.9856644 !16z L20vMDJuZF8
  • place:
Empire State Building = 40°44'54.4"N, 73°59'08.4"W
  • @location:
40.7413886,-73.9891371 = 40°44'29.0"N, 73°59'20.9"W - Flatiron Building
  • data-location:
40.7484405,-73.9856644 = 40°44'54.4"N, 73°59'08.4"W - Empire State Building

Result:

  • place = place name of the marker
  • @location = panorama location
  • data-location = place marker location

8. Web-API: Panorama preview

  • by location:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & location = 48.8589363,2.295527 & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY
  • by ID:
maps.googleapis.com/maps/api/streetview ? size = 640x360 & pano = CAoSLEFGMVFpcE81WUZKcGNmeVFjX1FBa0dUUWJ6VVh3eUdZcDFXRnB6SF9mVmFw & fov = 130 & heading = 18 & pitch = 30 & key = API_KEY

9. Web-API: Panorama metadata

  • by location:
maps.googleapis.com/maps/api/streetview/metadata ? location = 40.7143888,-74.0016005 & key=API_KEY
  • by ID:
maps.googleapis.com/maps/api/streetview/metadata ? pano = CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5 & key=API_KEY

Result:

{ "copyright" : "© Matthew MacVey", "date" : "2018-05", "location" : { "lat" : 40.7143888, "lng" : -74.0016005 }, "pano_id" : "CAoSLEFGMVFpcE41a2JtNDEzWEh3QzN5clJWWWZKSkFHYUlreGRPNVRDa0lGc1N5", "status" : "OK" }

10. Web-API: Place information

maps.googleapis.com/maps/api/place/textsearch/json ? query = Big-Ben & key = API_KEY

Result:

{     "html_attributions": [],     "results": [         {             "business_status": "OPERATIONAL",             "formatted_address": "London SW1A 0AA, United Kingdom",             "geometry": {                 "location": {                     "lat": 51.50072919999999,                     "lng": -0.1246254                 },                 "viewport": {                     "northeast": {                         "lat": 51.50211262989271,                         "lng": -0.12381395                     },                     "southwest": {                         "lat": 51.49941297010727,                         "lng": -0.1269553499999999                     }                 }             },             "icon": "https://maps.gstatic.com/mapfiles/place_api/icons/v1/png_71/generic_business-71.png",             "icon_background_color": "#7B9EB0",             "icon_mask_base_uri": "https://maps.gstatic.com/mapfiles/place_api/icons/v2/generic_pinlet",             "name": "Big Ben",             "photos": [                 {                     "height": 3970,                     "html_attributions": [                         "<a href=\"https://maps.google.com/maps/contrib/108563206122115577170\">Miguel Ángel Romacho Contreras</a>"                     ],                     "photo_reference": "AUacShixy_iNsH-O1SgdK6sCUEpqgarAI8y-xpQ942CNSVrqddsB6cIyQ19VE6v6UijDZRptsWhZqA3IgQSyZ2ewIq2nxdtVhy2ywoE5FD7T6w_tP5_4T18UZKCZIR84F0bemWL_TZiSKyDUbzPnke_ciTSgH8EsyMV_8BLz6x2IpQ0iGoBO",                     "width": 5955                 }             ],             "place_id": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",             "plus_code": {                 "compound_code": "GV2G+74 London, United Kingdom",                 "global_code": "9C3XGV2G+74"             },             "rating": 4.4,             "reference": "ChIJ2dGMjMMEdkgRqVqkuXQkj7c",             "types": [                 "tourist_attraction",                 "point_of_interest",                 "establishment"             ],             "user_ratings_total": 54585         }     ],     "status": "OK" }

11. Web-API: Sign your request

12. Android Place API

Places.initialize(appContext, mapsApiKey); String placeId = "ChIJT9j2eKVv5kcRp9MrU92slfY"; final PlacesClient placesClient = Places.createClient(activityContext); final List<Place.Field> fields = singletonList(Place.Field.LAT_LNG); final FetchPlaceRequest request = FetchPlaceRequest.newInstance(placeId, fields); placesClient.fetchPlace(request)   .addOnSuccessListener(response -> {     final Place place = response.getPlace();     final LatLng location = place.getLatLng());   }).addOnFailureListener(exception -> {     logger.error("Place not found:", exception);   });

13. Android Geocoder

final Geocoder geocoder = new Geocoder(appContext); String searchString = "Big Ben"; final List<Address> addresses = geocoder.getFromLocationName(searchString, 1); LatLng location = null; if (null != addresses && !addresses.isEmpty()) {   final Address address = addresses.get(0);   location = new LatLng(address.getLatitude(), address.getLongitude()); }

14. CID Issue

Sometimes, Google Maps app generate unusual link with parameter "cid":

maps.google.com/?cid=13226830714359798441

CID (Customer ID Number) is a unique Google identifier of a business entity. In other words, Google uses this number to identify an account of a business and cluster business information across GMB, Ads, Analytics, Maps and other Google’s properties.

Using a webservice API, you can retrieve information about a place using the specified identifier:

maps.googleapis.com/maps/api/place/details/json   ? cid = CID   & key = API_KEY

But this method is unsafe for use in an Android application because all the keys are stored within the app, and theoretically, an attacker could extract them from there. However, using a Maps API key can be secured at the Google Console level by specifying 'Set an application restriction' and 'Android Fingerprint restriction.' Unfortunately, such restrictions cannot be applied to a key used for retrieving information through a webservice, creating a theoretical vulnerability for third-party use of this key.

However, there is another way to obtain the necessary information. It involves making a request to the provided link, parsing the HTML page of the response body, and extracting the metadata containing the place's name. From there, it's straightforward – using a geocoder, we can find the location.

Meta-information in the response html:

<meta content="Big Ben · London SW1A 0AA, United Kingdom" itemprop="name"> <meta content="Big Ben · London SW1A 0AA, United Kingdom" property="og:title"> <meta content="Big Ben · London SW1A 0AA, United Kingdom" property="og:site_name">

Send request to CID-URL, receive response and extract meta from the body: https://gist.github.com/asilichenko/b0000eb1562c9e4e75b0d43d799260bc

import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import java.io.IOException; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import okhttp3.ResponseBody; public class MetadataExtractor { private static final String META_ITEMPROP_NAME = "meta[itemprop=name]"; private static final String META_PROPERTY_OG_TITLE = "meta[property=og:title]"; private static final String META_PROPERTY_OG_SITE_NAME = "meta[property=og:site_name]"; private static final String CONTENT = "content"; private final OkHttpClient httpClient; public MetadataExtractor() { this.httpClient = new OkHttpClient(); } public Metadata extract(String url) { final Request request = new Request.Builder().url(url).build(); try (final Response response = httpClient.newCall(request).execute()) { if (!response.isSuccessful()) { throw new RuntimeException("Request failed: " + response.code()); } Metadata retval = null; final ResponseBody body = response.body(); if (null != body) { final String html = body.string(); final Document doc = Jsoup.parse(html); final String name = getMetaContent(doc, META_ITEMPROP_NAME); final String title = getMetaContent(doc, META_PROPERTY_OG_TITLE); final String siteName = getMetaContent(doc, META_PROPERTY_OG_SITE_NAME); retval = new Metadata(name, title, siteName); } return retval; } catch (IOException ioe) { throw new RuntimeException(ioe); } } /** * Get an attribute value from the first matched element * that has the attribute. */ private String getMetaContent(Document doc, String selector) { return doc.select(selector).attr(CONTENT); } public static class Metadata { public final String name; public final String title; public final String siteName; public Metadata(String name, String title, String siteName) { this.name = name; this.title = title; this.siteName = siteName; } } }

15. Conclusion

In this article, I have presented various scenarios illustrating when a shared link on Google Maps can be generated.

Here are the conclusions I've drawn about the data in the links:

  • If a place or a marker is selected, then locations in the path and in the data both point to the selected place/marker.
  • Place name can be represented either as string or coordinates as pair of doubles or in DMS format.
  • If panorama is opened then panorama ID is present.
  • If a panorama is opened, then the @location equals the panorama's location; otherwise, the @location equals the map camera's location.
  • If the opened panorama belongs to the user, then the ID is useless, but the @location points to the panorama's location.
  • Mobile Google Maps currently does not support having both a selected marker and an opened panorama simultaneously. Therefore, all locations in the link are always consistent and point to user's desired location.

Now, let's explore the various types of Google Maps links I've identified. I've simplified them by removing unnecessary details to focus on the fundamental structure.

  • point -> web:
/maps/search/48.718116,+2.627758
  • point -> app:
/maps/place/ -13.165331,-72.548991/ data = !8m2 !3d -13.1653311 !4d -72.5489906
  • place -> web:
/maps/place/ Empire+State+Building/ @40.7321378,-73.9691401,17.87z/ data = !8m2 !3d 40.7484405 !4d -73.9856644
  • place + panorama:
/maps/place/ Empire+State+Building/ @40.7413886,-73.9891371,3a,75y,230.15h,116.97t/ data = !1s nhbQjZVsxbtfxb9rTUhZmQ !2e0 !8m2 !3d 40.7484405 !4d -73.9856644
  • place -> app:
/maps/place/Buckingham+Palace,+London+SW1A+1AA,+Great+Britain/
  • panorama:
/maps/ @48.858345,2.2943299,0a,75y,312.57h,84.6t/ data = !1s _BgQhZS8mY-Zix5BdCt2mA !2e0
  • query:
/maps?q=50.8136769,-2.4746919 /maps?q=London,+Great+Britain /maps?q=Gallagher+House,+2119+S+Homan+Ave,+Chicago,+IL+60623,+USA

Finally, let's define the general scheme of link structure based on coordinates and panorama ID:

/maps/search/COORDINATES /maps/search/STRING === /maps/place/NAME /maps/place/NAME/ @COORDINATES_MAP/ data=COORDINATES_PLACE /maps/place/NAME/ @COORDINATES_PANORAMA/ data=PANORAMA_ID !COORDINATES_PLACE /maps/place/ COORDINATES/ data=COORDINATES === /maps/@COORDINATES /maps/@COORDINATES/data=PANORAMA_ID === /maps?q=COORDINATES /maps?q=STRING === maps.google.com/?cid=CID /maps?cid=CID

Thanks for your time and best of luck with your mapping and panoramic adventures!

No comments:

Post a Comment

Why BQ28Z610 function Current() returns 0 mA

Fixing 0 mA Current Readings on the BQ28Z610 Device Custom driver for the BQ28Z610 device was connected directly via I2C. It is p...