Tour of Free(mium) HERE APIs

A short "teaser" presentation rushing through a small subset of many free APIs made by HERE Technologies under the Freemium plan. This notebook shows simple examples mostly for geocoding, places, maps and routing. They are designed for rapid consumption during a meetup talk. To that end, some code snippets longer than a few lines are imported from a module named utils.py. Third-party modules are imported in the respective sections below as needed. (See utils.py for a rough requirements list.)

Goal: Showing enough examples to wet you appetite for more, not delivering a polished "paper" or "package".

N.B.: This notebook is saved intentionally without cells executed as some of those would contain the HERE credentials used.

Freemium Plan

Setup

Credentials are imported from a here_credentials.py module if existing (via utils.py) defined as app_id and app_code, or from environment variables (HEREMAPS_APP_ID, HEREMAPS_APP_CODE).


In [ ]:
import random
import urllib

In [ ]:
import utils

In [ ]:
app_id   = utils.app_id
app_code = utils.app_code

berlin_lat_lon = [52.5, 13.4]

here_berlin_addr = 'Invalidenstr. 116, 10115 Berlin, Germany'

Geocoding

Raw REST


In [ ]:
import requests

In [ ]:
here_berlin_addr

In [ ]:
searchtext = urllib.parse.quote(here_berlin_addr)
searchtext

In [ ]:
url = (
     'https://geocoder.api.here.com/6.2/geocode.json'
    f'?searchtext={searchtext}&app_id={app_id}&app_code={app_code}'
)
utils.mask_app_id(url)

In [ ]:
obj = requests.get(url).json()
obj

In [ ]:
loc = obj['Response']['View'][0]['Result'][0]['Location']['DisplayPosition']
loc['Latitude'], loc['Longitude']

Geopy Plugin


In [ ]:
from geopy.geocoders import Here

In [ ]:
geocoder = Here(app_id, app_code)

In [ ]:
here_berlin_addr

In [ ]:
loc = geocoder.geocode(here_berlin_addr)
loc

In [ ]:
loc.latitude, loc.longitude

In [ ]:
loc.raw

In [ ]:
here_berlin_lat_lon = loc.latitude, loc.longitude
here_berlin_lat_lon

In [ ]:
loc = geocoder.reverse('{}, {}'.format(*here_berlin_lat_lon))
loc

In [ ]:
loc.latitude, loc.longitude

Places


In [ ]:
searchtext = 'Cafe'
lat, lon = here_berlin_lat_lon
url = (
     'https://places.api.here.com/places/v1/autosuggest'
    f'?q={searchtext}&at={lat},{lon}'
    f'&app_id={app_id}&app_code={app_code}'
)
utils.mask_app_id(url)

In [ ]:
obj = requests.get(url).json()
obj

In [ ]:
for p in [res for res in obj['results'] if res['type']=='urn:nlp-types:place']:
    print('{!r:23} {:4d} m  {}'.format(p['position'], p['distance'], p['title']))

Maps

Single Map Tiles


In [ ]:
from IPython.display import Image

In [ ]:
(lat, lon), zoom = berlin_lat_lon, 10
xtile, ytile = utils.deg2tile(lat, lon, zoom)
xtile, ytile

In [ ]:
# %load -s deg2tile utils
def deg2tile(lat_deg, lon_deg, zoom):
    lat_rad = radians(lat_deg)
    n = 2.0 ** zoom
    xtile = int((lon_deg + 180.0) / 360.0 * n)
    ytile = int((1.0 - log(tan(lat_rad) + (1 / cos(lat_rad))) / pi) / 2.0 * n)
    return (xtile, ytile)


# not used here

In [ ]:
tiles_url = utils.build_here_tiles_url(
    maptype='base',
    tiletype='maptile',
    scheme='normal.day',
    x=xtile,
    y=ytile,
    z=zoom)

In [ ]:
utils.mask_app_id(tiles_url)

In [ ]:
img = Image(url=tiles_url)
img

In [ ]:
# %load -s build_here_tiles_url utils
def build_here_tiles_url(**kwdict):
    """
    Return a HERE map tiles URL, based on default values that can be 
    overwritten by kwdict...
    
    To be used for map building services like leaflet, folium, and 
    geopandas (with additional fields inside a dict)...
    """
    params = dict(
        app_id     = app_id,
        app_code   = app_code,
        maptype    = 'traffic',
        tiletype   = 'traffictile',
        scheme     = 'normal.day',
        tilesize   = '256',
        tileformat = 'png8',
        lg         = 'eng',
        x          = '{x}',
        y          = '{y}',
        z          = '{z}',
        server     = random.choice('1234')
    )
    params.update(kwdict)
    url = (
        'https://{server}.{maptype}.maps.api.here.com'
        '/maptile/2.1/{tiletype}/newest/{scheme}/{z}/{x}/{y}/{tilesize}/{tileformat}'
        '?lg={lg}&app_id={app_id}&app_code={app_code}'
    ).format(**params)
    return url

Full Maps


In [ ]:
import folium

In [ ]:
folium.Map(location=berlin_lat_lon, zoom_start=10, tiles='Stamen Terrain')

In [ ]:
m = folium.Map(location=berlin_lat_lon, zoom_start=10)
folium.GeoJson('stops_berlin.geojson', name='BVG Stops').add_to(m)
folium.LayerControl().add_to(m)
m

Now HERE


In [ ]:
tiles_url = utils.build_here_tiles_url()

In [ ]:
utils.mask_app_id(tiles_url)

In [ ]:
folium.Map(
    location=berlin_lat_lon, 
    zoom_start=10, 
    tiles=tiles_url, 
    attr='HERE.com')

Geocoding Revisited

  • more GIS-savvy
  • (a litlle) more geo-spatial smarts

In [ ]:
%matplotlib inline

In [ ]:
import geopandas
import shapely
import shapely.wkt
from geopy.geocoders import Here

In [ ]:
geocoder = Here(app_id, app_code)

In [ ]:
here_berlin_addr

In [ ]:
loc = geocoder.geocode(
    here_berlin_addr, 
    additional_data='IncludeShapeLevel,postalCode') # <- get shapes!
loc.raw

In [ ]:
wkt_shape = loc.raw['Location']['Shape']['Value']

In [ ]:
shape = shapely.wkt.loads(wkt_shape)
shape

In [ ]:
type(shape)

In [ ]:
here_berlin_point = shapely.geometry.Point(*reversed(here_berlin_lat_lon))
here_berlin_point

In [ ]:
shape.contains(here_berlin_point)

In [ ]:
shape.contains(shapely.geometry.Point(0, 0))

In [ ]:
data = [
    ['10115 Berlin', shape], 
    ['HERE HQ', here_berlin_point]
]
df = geopandas.GeoDataFrame(data=data, columns=['object', 'geometry'])
df

In [ ]:
url = utils.build_here_tiles_url(x='tileX', y='tileY', z='tileZ')

In [ ]:
utils.mask_app_id(url)

In [ ]:
df.crs = {'init': 'epsg:4326'}   # dataframe is WGS84
ax = df.plot(figsize=(10, 10), alpha=0.5, edgecolor='k')
utils.add_basemap(ax, zoom=15, url=url)

In [ ]:
# %load -s add_basemap utils
def add_basemap(ax, zoom, url='http://tile.stamen.com/terrain/tileZ/tileX/tileY.png'):
    # Special thanks to Prof. Martin Christen at FHNW.ch in Basel for
    # his GIS-Hack to make the output scales show proper lat/lon values!
    xmin, xmax, ymin, ymax = ax.axis()
    basemap, extent = ctx.bounds2img(xmin, ymin, xmax, ymax, zoom=zoom, ll=True, url=url)
    
    # calculate extent from WebMercator to WGS84
    xmin84, ymin84 = Mercator2WGS84(extent[0], extent[2])
    xmax84, ymax84 = Mercator2WGS84(extent[1], extent[3])
    extentwgs84 = (xmin84, xmax84, ymin84, ymax84)
    
    ax.imshow(basemap, extent=extentwgs84, interpolation='bilinear')
    # restore original x/y limits
    ax.axis((xmin, xmax, ymin, ymax))

Routing


In [ ]:
from ipyleaflet import Map, Marker, CircleMarker, Polyline, basemap_to_tiles
from ipywidgets import HTML

In [ ]:
here_berlin_addr

In [ ]:
here_berlin_lat_lon

In [ ]:
dt_oper_berlin_addr = 'Bismarkstr. 35, 10627 Berlin, Germany'

In [ ]:
loc = geocoder.geocode(dt_oper_berlin_addr)
dt_oper_berlin_lat_lon = loc.latitude, loc.longitude
dt_oper_berlin_lat_lon

In [ ]:
route = utils.get_route_positions(
    here_berlin_lat_lon, 
    dt_oper_berlin_lat_lon,
    mode='fastest;car;traffic:disabled',
    language='en')

In [ ]:
route

In [ ]:
center = utils.mid_point(
    here_berlin_lat_lon, 
    dt_oper_berlin_lat_lon)
here_basemap = utils.build_here_basemap()
layers = [basemap_to_tiles(here_basemap)]
m = Map(center=center, layers=layers, zoom=13)
m

In [ ]:
route[0]['shape'][:4]

In [ ]:
path = list(utils.chunks(route[0]['shape'], 2))
path[:2]

In [ ]:
sum(map(lambda pq: utils.geo_distance(*pq), list(utils.pairwise(path))))

In [ ]:
m += Polyline(locations=path, color='red', fill=False)

In [ ]:
for man in route[0]['leg'][0]['maneuver']:
    lat = man['position']['latitude']
    lon = man['position']['longitude']
    desc = man['instruction']
    marker = Marker(location=(lat, lon), draggable=False)
    marker.popup = HTML(value=desc)
    m += marker

In [ ]:
for lat, lon in path:
    m += CircleMarker(location=(lat, lon), radius=3, color='blue')

In [ ]:
reverse_route = utils.get_route_positions(
    dt_oper_berlin_lat_lon,
    here_berlin_lat_lon,
    mode='shortest;pedestrian',
    language='en')

In [ ]:
utils.add_route_to_map(reverse_route, m)

In [ ]:
path = list(utils.chunks(reverse_route[0]['shape'], 2))
sum(map(lambda pq: utils.geo_distance(*pq), list(utils.pairwise(path))))

In [ ]:
# %load -s add_route_to_map utils.py
def add_route_to_map(route, some_map, color='blue'):
    """
    Add a route from the HERE REST API to the given map.
    
    This includes markers for all points where a maneuver is needed, like 'turn left'.
    And it includes a path with lat/lons from start to end and little circle markers
    around them.
    """
    path_positions = list(chunks(route[0]['shape'], 2))
    maneuvers = {
        (man['position']['latitude'], man['position']['longitude']): man['instruction']
            for man in route[0]['leg'][0]['maneuver']}

    polyline = Polyline(
        locations=path_positions,
        color=color,
        fill=False
    )
    some_map += polyline
    
    for lat, lon in path_positions:
        if (lat, lon) in maneuvers:
            some_map += CircleMarker(location=(lat, lon), radius=2)
            
            marker = Marker(location=(lat, lon), draggable=False)
            message1 = HTML()
            message1.value = maneuvers[(lat, lon)]
            marker.popup = message1
            some_map += marker
        else:
            some_map += CircleMarker(location=(lat, lon), radius=3)

Isolines


In [ ]:
import requests
import ipywidgets as widgets

In [ ]:
lat, lon = here_berlin_lat_lon
url = (
    'https://isoline.route.api.here.com'
    '/routing/7.2/calculateisoline.json'
   f'?app_id={app_id}&app_code={app_code}' 
   f'&start=geo!{lat},{lon}'
    '&mode=fastest;car;traffic:disabled'
    '&range=300,600'  # seconds/meters
    '&rangetype=time' # time/distance
    #'&departure=now' # 2013-07-04T17:00:00+02
    #'&resolution=20' # meters
)
obj = requests.get(url).json()

In [ ]:
obj

In [ ]:
here_basemap = utils.build_here_basemap()
layers = [basemap_to_tiles(here_basemap)]
m = Map(center=(lat, lon), layers=layers, zoom=12)
m

In [ ]:
m += Marker(location=(lat, lon))

In [ ]:
for isoline in obj['response']['isoline']:
    shape = isoline['component'][0]['shape']
    path = [tuple(map(float, pos.split(','))) for pos in shape]
    m += Polyline(locations=path, color='red', weight=2, fill=True)

More interactively


In [ ]:
here_basemap = utils.build_here_basemap()
layers = [basemap_to_tiles(here_basemap)]
m = Map(center=(lat, lon), layers=layers, zoom=13)
m

In [ ]:
lat, lon = here_berlin_lat_lon

In [ ]:
dist_iso = utils.Isoline(m, 
                         lat=lat, lon=lon, 
                         app_id=app_id, app_code=app_code)

In [ ]:
# can't get this working directly on dist_iso with __call__ :(
def dist_iso_func(meters=1000):
    dist_iso(meters=meters)

In [ ]:
widgets.interact(dist_iso_func, meters=(1000, 2000, 200))

In [ ]:
# %load -s Isoline utils
class Isoline(object):
    def __init__(self, the_map, **kwdict):
        self.the_map = the_map
        self.isoline = None
        self.url = (
            'https://isoline.route.api.here.com'
            '/routing/7.2/calculateisoline.json'
            '?app_id={app_id}&app_code={app_code}' 
            '&start=geo!{lat},{lon}'
            '&mode=fastest;car;traffic:disabled'
            '&range={{meters}}'  # seconds/meters
            '&rangetype=distance' # time/distance
            #'&departure=now' # 2013-07-04T17:00:00+02
            #'&resolution=20' # meters
        ).format(**kwdict)
        self.cache = {}

    def __call__(self, meters=1000):
        if meters not in self.cache:
            print('loading', meters)
            url = self.url.format(meters=meters)
            obj = requests.get(url).json()
            self.cache[meters] = obj
        obj = self.cache[meters]
        isoline = obj['response']['isoline'][0]
        shape = isoline['component'][0]['shape']
        path = [tuple(map(float, pos.split(','))) for pos in shape]
        if self.isoline:
            self.the_map -= self.isoline
        self.isoline = Polyline(locations=path, color='red', weight=2, fill=True)
        self.the_map += self.isoline

More to come... (in another meetup ;)

  • dynamic map content (based on traitlets)
  • streaming data
  • ZeroMQ integration
  • sneak preview below

In [ ]:
here_basemap = utils.build_here_basemap()
layers = [basemap_to_tiles(here_basemap)]
m = Map(center=berlin_lat_lon, layers=layers, zoom=13)
m

In [ ]:
marker = Marker(location=berlin_lat_lon)
marker.location

In [ ]:
m += marker

In [ ]:
m -= marker

In [ ]:
m += marker

In [ ]:
marker.location = [52.49, 13.39]

In [ ]:
loc = marker.location
for i in range(5000):
    d_lat = (random.random() - 0.5) / 100
    d_lon = (random.random() - 0.5) / 100
    marker.location = [loc[0] + d_lat, loc[1] + d_lon]

Take-Aways

  • HERE Freemium rocks!
  • Jupyter rocks!
  • Jupyter Lab rocks! (not shown here ;)
  • GeoPandas rocks! (not much shown here ;)
  • Ipyleaflet rocks!
  • Ipywidgets rock!

Q: With all of this in your hands, what will you rock?!