import os
msg = "Error: Environment variable {} not found"
for varname in ["HEREMAPS_APP_ID", "HEREMAPS_APP_CODE"]:
    assert os.getenv(varname), msg.format(varname)

import folium
import requests
import ipywidgets

print("You are ready to go!")

"""
Explanation: HERE Map Tiles Rest API Explorer
This notebook is intended to show how to access map tiles from HERE Technologies using their RESTful Map Tile API and build a variety of maps using Folium, a high-level library for creating web maps with Leaflet.
Folium supports already a number of map tiles providers, including OpenStreetMap or Mapbox (the latter needing an API_KEY), but not yet those made by HERE. To see such HERE map tiles and maps inside this Jupyter notebook you will need to register for a free HERE account. You can chose a 90-day free trial or a free public "BASIC" plan with standard features (see more about plans). And then you can obtain an APP_CODE and APP_ID for accessing HERE maps.
Running this notebook also requires to have pip-installed the external packages named requests, folium, and ipywidgets (Python 3 is recommended, but 2 should also work). The APP_CODE and APP_ID values are assumed to be set as environment variables named c and HEREMAPS_APP_ID, respectively. These will be included in the HTML maps created with Folium/Leaflet, so be careful where you share your results!
N.B. When writing this notebook it appeared that you can get HERE map tiles even with fake APP_CODE and APP_ID like foo and bar (as stored inside some cells in this notebook), but it is unpredictable to say which ones you will get or not. This is likely a way for HERE to show something even without credentials, while making the result not really useful, instead of limiting zoom levels or using watermarks (which might be a costly thing when serving many map tiles). Anyway, you can have a free account, see above.
You will see the output best if you run the notebook locally with proper HERE credentials. But if you read it on the Jupyter Notebook viewer or on GitHub, or if you don't have real HERE credentials you might actually see no output for some cells. In these cases there is a comment to explain and a static screenshot is provided, too.
Finally, it is assumed that you have a little experience with Jupyter notebooks, so not all steps are explained in detail. That was long intro, so now let's start playing a bit with map tiles and build maps! 
Testing your environment
First, run a little test to see if you are ready to go. Make sure you address any import or assertion error appearing in the next cell (by pip-installing the missing packages or adding the missing environment variables). Then move on!
End of explanation
"""


import os
from IPython.display import Image

# load HERE API credentials, get your own from http://developer.here.com
app_id, app_code = map(os.getenv, ["HEREMAPS_APP_ID", "HEREMAPS_APP_CODE"])

# then get and display a specific map tile
url = "https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/13/4400/2686/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code)
Image(url=url)

"""
Explanation: Getting single HERE map tiles
Web maps are composed of individual so-called map tiles, traditionally small precomputed bitmaps (although the industry is moving into using so-called map vector tiles, which is not a topic in this notebook, though). Now, lets see how to get a single map tile using the HERE REST API and your credentials. The URL contains quite a few parameters describing which tile exactly to download, but more about that later:
End of explanation
"""


import requests

resp = requests.get(url)
print(resp.status_code)
dict(resp.headers.items())

"""
Explanation: Static version
If you don't see the tile image in the section above it's likely because you read this notebook on GitHub or on the Jupyter NoteBook Viewer. Subsequent sections will include static versrions included inside this notebook. 
Inspecting HTTP response header
Have a look at the HTTP response headers, too! Here it's all fine.
End of explanation
"""


# Conversion between lat/lon in degrees (and zoom) to x/y/zoom as used in tile sets,
# from http://wiki.openstreetmap.org/wiki/Slippy_map_tilenames#Python

from math import radians, degrees, log, cos, tan, pi, atan, sinh

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)

def tile2deg(xtile, ytile, zoom):
    n = 2.0 ** zoom
    lon_deg = xtile / n * 360.0 - 180.0
    lat_rad = atan(sinh(pi * (1 - 2 * ytile / n)))
    lat_deg = degrees(lat_rad)
    return (lat_deg, lon_deg)

"""
Explanation: Getting specific map tiles
Normally you want to get a map tile that contains a specific geographic position at some zoom level. Depending on this zoom level a map of the entire world is composed of a varying number of tiles with x- and y-positions on a rectangular grid. You can use two simple functions to convert between these two domains, geographic coordinates on a sphere and the respective tile positions on a rectangular grid (including zoom level):
End of explanation
"""


deg2tile(52.518611, 13.408333, 14)

tile2deg(8802, 5373, 14)

deg2tile(52.522905940278065, 13.4033203125, 14)

"""
Explanation: Let's make a round-trip test with a the latitude and longitude of a geographic point somewhere in the middle of Berlin, Germany (taken from the respective Wikipedia page):
End of explanation
"""


url = "https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/14/8802/5373/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code)
Image(url=url)

"""
Explanation: And now let's get the map tile containing this geographic point (52.518611, 13.408333) somewhere inside, at level zoom 14:
End of explanation
"""


from ipywidgets import interact

schemes = ["normal.day", "normal.night"]

@interact(lat=(-90., 90.), lon=(-180., 180.), zoom=(0, 18), scheme=schemes, show_url=False)
def get_here_maptile(lat=52.518611, lon=13.408333, zoom=11, scheme="normal.day", show_url=False):
    x, y = deg2tile(lat, lon, zoom)
    params = dict(x=x, y=y, zoom=zoom, scheme=scheme, app_id=app_id, app_code=app_code)
    url = "https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/{scheme}/{zoom}/{x}/{y}/256/png8?lg=eng&app_id={app_id}&app_code={app_code}".format(**params)
    if show_url:
        print(url)
    return Image(url=url)

"""
Explanation: Adding an interactive interface
You can make this easier to explore by using a few little widgets as an interactive interface for some of these parameters. Here we take only latitude, longitude, zoom level and only two scheme names (the API documentation lists many more!).
End of explanation
"""


import folium; folium.Map(location=(52.518611, 13.408333), zoom_start=14)

"""
Explanation: Static version
If you don't see the widgets and/or image output in the section above it's likely because you read this notebook on GitHub or on the Jupyter NoteBook Viewer and/or you don't have valid HERE credentials. In this case, this is a static sample image of what you see when running the notebook locally with valid HERE credentials: 
<img src="images/here9.png" align="left" width="305px">
Building entire maps
Of course, the whole purpose of map tiles is to build entire maps from a bunch of them, like those you know from HERE or OpenStreetMap. All the magic of map tile selection and assembly happens in the background if you use the right tool, like Leaflet and the excellent Folium interface to it. Effectively, using Folium you can do it in one line of code (the default map tiles are set to tiles="OpenStreetMap", but you can change that):
End of explanation
"""


import folium
m = folium.Map(location=(52.518611, 13.408333), zoom_start=14)
m.save("mymap.html")

"""
Explanation: Any such map output can be saved as a self-contained HTML file (without the map tiles, of course) and easily passed on to somebody else. (This other person might have to use her own credentials, if needed, though.):
End of explanation
"""


import folium
from ipywidgets import interact

# Cloudmade Mapbox needs an API key, Mapbox Control Room is limited to a few levels
tiles = [name.strip() for name in """
    OpenStreetMap
    Mapbox Bright
    Mapbox Control Room
    Stamen Terrain
    Stamen Toner
    Stamen Watercolor
    CartoDB positron
    CartoDB dark_matter""".strip().split('\n')]

@interact(lat=(-90., 90.), lon=(-180., 180.), tiles=tiles, zoom=(1, 18))
def create_map(lat=52.518611, lon=13.408333, tiles="Stamen Toner", zoom=10):
    return folium.Map(location=(lat, lon), tiles=tiles, zoom_start=zoom)

"""
Explanation: Making it interactive
One nice feature of Folium is that you can chose between a number of predefined tile sets by using the tiles parameter for folium.Map, so we can easily switch between those in a more interactive way, too. In addition, the following snippet lets you also change latitude, longitude and zoom level interactively:
End of explanation
"""


import os
import folium

c = map(os.getenv, ["HEREMAPS_APP_ID", "HEREMAPS_APP_CODE"])
tiles="https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code)
folium.Map(location=(52.518611, 13.408333), tiles=tiles, zoom_start=10, attr="HERE.com")

"""
Explanation: Static version
If you don't see the widgets and/or map output in the section above it's likely because you read this notebook on GitHub or on the Jupyter NoteBook Viewer. In this case, this is a static sample image of what you see when running the notebook locally with valid HERE credentials: 

And one thing missing here is, of course: when you interact with the map itself by zooming in or out or panning around you would like to see the widget values change accordingly. But that would need some JavaScript magic. Your contribution is welcome!
Building HERE Maps
But Folium goes one step further and lets you use an arbitrary source of map tiles, simply by providing a URL for the tiles parameter in folium.Map() that needs to contain placeholders for tile numbers and the zoom level. This means you can use HERE map tiles, even without built-in support for them in Folium.
End of explanation
"""


import os
import datetime

app_id, app_code = map(os.getenv, ["HEREMAPS_APP_ID", "HEREMAPS_APP_CODE"])

tile_formats = "png8 png jpeg".split()
map_types = "base aerial pano traffic".split()
tile_types = """
    maptile traffictile flowbasetile trucktile rcdistonlytile
    mapnopttile blinetile alabeltile
""".strip().split()
languages_3 = "eng ger fre ita gre rus ara hin chi ---".split()

schemes = """
    normal.day normal.day.grey normal.day.transit normal.night
    normal.day.mobile
    pedestrian.day pedestrian.day.mobile
    carnav.day.grey
    normal.traffic.day
    reduced.night
    satellite.day
    hybrid.day
    wrong
""".strip().split()


class HereMap(object):
    def draw(self, map_type, tile_type, scheme, tile_format, language,
             lat, lon, zoom=10, show_url=False):
        "Draw a HERE map. Default values are given as parameter keyword values."
        
        # raise error for not allowed combinations
        if scheme == "wrong":
            raise Exception("Combination not allowed")
        
        # set parameter defaults for map tiles to request
        number = '1' # (1-4)
        map_version = "newest" # or some hex hash value
        server_env = "" # production, or "cit." for "customer integration test"
        tile_size = "256"

        # build map tiles URL
        server = "https://{number}.{map_type}.maps.{server_env}api.here.com"
        path = "/maptile/2.1/{tile_type}/{map_version}/{scheme}/{{z}}/{{x}}/{{y}}/{tile_size}/{tile_format}"
        query = "?app_id={app_id}&app_code={app_code}"
        if language != "---":
            query += "&lg={language}"
        params = dict(number=number, map_type=map_type, tile_type=tile_type,
            scheme=scheme, tile_format=tile_format, tile_size=tile_size,
            map_version=map_version, server_env=server_env,
            app_id=app_id, app_code=app_code,
            language=language
        )
        tiles_url = (server + path + query).format(**params)

        # set map attribution text
        year = datetime.datetime.now().year
        attr = 'Data by <a href="http://developer.here.com">HERE.com</a>, %s' % year

        if show_url:
            print(tiles_url)

        # build and return a map
        return folium.Map(
            location=(lat, lon),
            tiles=tiles_url,
            zoom_start=zoom,
            attr=attr
        )

hmap = HereMap()
interact(hmap.draw, map_type=map_types, tile_type=tile_types, scheme=schemes,
         tile_format=tile_formats, language=languages_3, lat=52.5159, lon=13.3777, zoom=(1, 18), show_url=False)

"""
Explanation: Static version
If you don't see the widgets and/or map output in the section above it's likely because you read this notebook on GitHub or on the Jupyter Notebook Viewer and/or you don't have valid HERE credentials. In this case, this is a static sample image of what you see when running the notebook locally with valid HERE credentials: 

Adding more interactive features
Now let's build a more feature-rich interactive interface to explore the API. This time there are more parameters to play with, using preselected values for some of them. Note that not all combinations are meaningful. Where they are not you simply don't see a map result below. There could be some validation, but... there isn't. This time we use a class, but that's not strictly needed.
End of explanation
"""


import folium
from ipywidgets import interact

cities = ["Berlin", "Paris", "Chicago", "Singapore"]
examples = ["Traffic", "Truck info", "Transit", "Regular", "Satellite"]
@interact(city=cities, example=examples)
def show_canned_examples(city, example):
    attr = "HERE.com"
    latlon_for_city = {
        "Berlin": (52.518611, 13.408333), 
        "Paris": (48.8567, 2.3508), 
        "Chicago": (41.88416, -87.63243),
        "Singapore": (1.283333, 103.833333)
    }
    zoom = 14
    queries = {
        "Traffic":
            "https://1.traffic.maps.api.here.com/maptile/2.1/traffictile/newest/normal.traffic.day/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code),
        "Regular":
            "https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code),
        "Truck info":
            "https://1.base.maps.api.here.com/maptile/2.1/trucktile/newest/normal.day.grey/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code),
        "Transit":
            "https://1.base.maps.api.here.com/maptile/2.1/maptile/newest/normal.day.transit/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code),
        "Satellite":
            "https://1.aerial.maps.api.here.com/maptile/2.1/maptile/newest/hybrid.day/{z}/{x}/{y}/256/png8?lg=eng&app_id=%s&app_code=%s" % (app_id, app_code),
    }
    return folium.Map(location=latlon_for_city[city], tiles=queries[example],attr=attr, zoom_start=zoom)

"""
Explanation: Static version
If you don't see the widgets and/or map output in the section above it's likely because you read this notebook on GitHub or on the Jupyter NoteBook Viewer and/or you don't have valid HERE credentials. In this case, this is a static sample image of what you see when running the notebook locally with valid HERE credentials: 

Building a map dashboard
Once you have explored the map parameter space long enough, you might arrive at a few settings that you would probably need more regularly. This is the moment when you can prepare such queries into something like a very simple dashboard-like overview as shown below. This one shows a few maps displaying traffic conditions, information for trucks, public transit and a normal and hybrid satellite view for a few selected cities.
End of explanation
"""


