MatthewDaws / TileMapBase

Use OpenStreetMap tiles as basemaps in python / matplotlib
MIT License
88 stars 20 forks source link

Is there a high level API? #9

Open kota7 opened 5 years ago

kota7 commented 5 years ago

Thank you for the project. I wonder if there is a high-level API for plotting that abstracts (automates) the choice of extent and projection?

When plotting on a map, one needs to

I think it would be convenient if there is an API that automates these configuration/preparation steps. Is there one, or if not, is there a plan?

jean-claudeF commented 4 years ago

Hi Matthew and kota7, I use tilemapbase like this: (The example code creates a Tkinter frame containing a map. I use this for a database project)

""" tilemapbase display simple map"""
# https://github.com/MatthewDaws/TileMapBase

"""
22.6.2019
There was a problem loading OSM maps
https://help.openstreetmap.org/questions/24740/why-am-i-all-of-a-sudden-getting-error-403
Workaround: use OSM_Humanitarian or Stamen
tile source can be set (when not set, default set in edit area is used

12.3.2019
It seems to be better to set the pixelwidth resolution than the zoomfactor
If the zoomfactor is used, the image has less details when zooming in
If pixelwidth is used the zoomfactor is automatically changed when zooming

11.3.2019
some experiments:
zoom with mouse -> ?

TODO: zoom with constant pixelwidth makes more sense!

Done: zoom with mouse wheel
Todo: move with mouse
problem: slow!
"""

# imports
import tilemapbase
import matplotlib.pyplot as plt

import tkinter as tk

# Tkinter backend for matplotlib:
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg      ##, NavigationToolbar2TkAgg

# implement the default mpl key bindings:
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure

import numpy as np
import time 

#-------------------------------------------------------------

## EDIT AREA

# use pixelwidth or zoomlevel in plotter = tilemapbase.Plotter(extent, t, ...)
pixelwidth=600          # the higher the finer the map
zoomlevel=7

size=(200,150)          # window size in mm            

# coordinates
my_location = (-4.8, 38)        # center location
loc2=(-4.3, 38.5)

dlonlat=(4.0, 2.0)              # delta (lon, lat) around center location

# map source:
tilesdefault = 'build_OSM()'

# END EDIT AREA

#-----------------------------------------------------------
## Geographic helper functions:

def getgeowindow(location, deltalonlat):
    """input: location = (lon, lat)
              deltalonlat = dlon, dlat
       returns: (lon1,lon2,lat1,lat2) window"""  
    dlon, dlat = deltalonlat          
    lon, lat = location
    (lon1,lon2,lat1,lat2) = (lon-dlon, lon+dlon, lat-dlat, lat+dlat)
    return (lon1,lon2,lat1,lat2)
#...................................................................    
def shift_geowindow(window, shift):
    """ input: geowindow = (lon1, lon2, lat1, lat2)
               shift = dlon, dlat
       returns: new (lon1,lon2,lat1,lat2) window"""  
    dlon, dlat = shift  
    (lon1,lon2,lat1,lat2) = window  
    (lon1,lon2,lat1,lat2) = (lon1+dlon, lon2+dlon, lat1+dlat, lat2+dlat)   
    return (lon1,lon2,lat1,lat2)
#....................................................................
def move_geowindow(window, dnorth, deast):
    ''' relative movement
    dnorth, deast -1...0...+1 (relative to window)
    '''
    (lon1,lon2,lat1,lat2) = window
    # width + height of geowindow:
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    dlon *= deast
    dlat *= dnorth
    (lon1,lon2,lat1,lat2) = (lon1+dlon, lon2+dlon, lat1+dlat, lat2+dlat)
    return (lon1,lon2,lat1,lat2)  
#....................................................................        
def zoom_geowindow(window, zfactor):
    (lon1,lon2,lat1,lat2) = window
    # width + height of geowindow:
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    # middle:
    lonM = lon1 + dlon/2
    latM = lat1 + dlat/2
    # new width + height:
    dlon = dlon * zfactor
    dlat = dlat * zfactor
    # new geowindow
    (lon1,lon2,lat1,lat2) = (lonM-dlon/2, lonM+dlon/2, latM-dlat/2, latM+dlat/2)   
    return (lon1,lon2,lat1,lat2) 

#------------------------------------------------------------

''' Class MapWindow: window to display map'''

class Mapwindow():
    def __init__(self, masterframe, size, tiles=tilesdefault): 
        """ Create a map window, set tiles source: OSM
                size = (width, height) in mm
        """

        # create database (at first run)
        tilemapbase.init(create=True)

        # Use open street map or other tiles   (problem OSM 22.6.19!)
        #self.tiles = tilemapbase.tiles.OSM             ## problem: access forbidden 22.6.19
        #self.tiles = tilemapbase.tiles.OSM_Humanitarian ## workaround 22.6.19
        s='self.tiles = tilemapbase.tiles.' + tiles
        print(s)
        exec(s)

        #self.tiles = tilemapbase.tiles.build_OSM_Humanitarian()
        #self.tiles = tilemapbase.tiles.Stamen_Terrain

        # create canvas:
        (w,h)=size    
        inchsize=(w/25.4, h/25.4)
        self.figure = Figure(inchsize)
        self.fig=Figure(figsize=inchsize, tight_layout=True)
        self.ax=self.fig.add_subplot(111)

        #self.ax.xaxis.set_visible(False)
        #self.ax.yaxis.set_visible(False)

        self.canvas = FigureCanvasTkAgg(self.fig, master=masterframe)
        self.canvas.get_tk_widget().pack()

        # event binding: must be done by mpl_connect, not by bind!
        self.canvas.mpl_connect('button_press_event', self.onclick)
        self.canvas.mpl_connect('scroll_event', self.onscroll)

    # mouse event handling   (not ready)
    # zoom with mouse scroll OK
    # move E, W would be simple with button 1,3
    # but how would we do move NS ??
    def onclick(self, event):
        print(event)
    def onscroll(self, event):
        #print(event)  
        print (event.button)
        if event.button == "up":
            self.redraw_zoom(0.8)  
        else:
            self.redraw_zoom(1.2)      

    def set_lonlat(self,geowindow, aspect=False):
        """set geographic coordinates window for the map
           if aspect=True: force quadratic aspect"""
        self.geowindow = geowindow   
        lon1,lon2,lat1,lat2 = geowindow   
        self.extent = tilemapbase.Extent.from_lonlat(lon1,lon2,lat1,lat2)
        if aspect:
            self.extent = self.extent.to_aspect(1.0)
        self.geowindow = lon1,lon2,lat1,lat2
        print ("GEOWINDOW: ",self.geowindow)

    def drawtiles_z(self, zoomlevel):
        """Draw the map with a certain zoomlevel"""

        # convert extent to a collection of tiles:
        #   use pixelwidth or zoomlevel in plotter = tilemapbase.Plotter(extent, t, ...)
        ##plotter = tilemapbase.Plotter(extent, t, width=pixelwidth)
        self.plotter = tilemapbase.Plotter(self.extent, self.tiles, zoom=zoomlevel)

        # assemble tiles to 1 image (using PIL) and plot them with matplotlib:
        self.plotter.plot(self.ax, self.tiles)
        self.zoomlevel = self.plotter.zoom

    def drawtiles(self, pixelwidth):
        """Draw the map with a certain pixelwidth
        For varying zoom in / out this may be better than to use a fixed zoomlevel
        The zoom level is automatically determined and found in self.zoomlevel"""
        self.pixelwidth = pixelwidth 
        # convert extent to a collection of tiles:
        #   use pixelwidth or zoomlevel in plotter = tilemapbase.Plotter(extent, t, ...)
        self.plotter = tilemapbase.Plotter(self.extent, self.tiles, width=pixelwidth)
        self.zoomlevel=self.plotter.zoom
        print("actual zoom level: ", self.plotter.zoom)

        # assemble tiles to 1 image (using PIL) and plot them with matplotlib:
        self.plotter.plot(self.ax, self.tiles)    

    def redraw(self, clearmarks=False):
        """Redraw the map 
                clearmarks = True -> erase marks before redrawing
                clearmarks = False -> leave marks"""
        if clearmarks==True:
            self.ax.cla()
        self.drawtiles(self.pixelwidth) 
        self.canvas.draw()

    def redraw_shift(self, dlonlat, clearmarks=False):
        """ Shift map  about dlonlat = (dlon, dlat) in degrees"""   
        if clearmarks==True:
            self.ax.cla()

        gw= shift_geowindow(self.geowindow, dlonlat)
        self.set_lonlat(gw)
        self.drawtiles(self.pixelwidth) 
        self.canvas.draw()

    def redraw_move(self, dnorth, deast, clearmarks=False):
        """ Relative shift map  about dnorth, deast -1....+1 (relative to window) """   
        if clearmarks==True:
            self.ax.cla()

        gw= move_geowindow(self.geowindow, dnorth, deast)
        self.set_lonlat(gw)
        self.drawtiles(self.pixelwidth) 
        self.canvas.draw()

    def redraw_zoom(self, zoomfactor, clearmarks=False):
        ''' narrow or widen the extent of the map window by zoomfactor''' 
        # eventually clear old marks (or not)
        if clearmarks==True:
            self.ax.cla()

        gw= zoom_geowindow(self.geowindow, zoomfactor)
        self.set_lonlat(gw)
        self.drawtiles(pixelwidth)  
        self.canvas.draw()

    def mark(self, location, color="red", marker=".", linewidth=10, clearold=False):    
        """Mark location on the map
           location = (longitude, latitude)"""
        # eventually clear old marks (or not)
        if clearold:
            self.redraw(clearmarks=True)   

        # mark on map
        x, y = tilemapbase.project(*location)
        self.ax.scatter(x,y, marker=marker, color=color, linewidth=linewidth)
        self.canvas.draw() 

        # remember last location
        self.lastlocation = location

#--------------------------------------------------
class MapFrame():
    def __init__(self, masterframe, size, pixelwidth, geowindow, tiles = tilesdefault): 
        """ Create a map window, set tiles source: OSM
                size = (width, height) in mm
            with buttons to zoom + move    
        """
        myframe=tk.Frame(master=masterframe)
        self.map = Mapwindow(myframe,size, tiles)

        #pw.set_lonlat(lon1,lon2,lat1,lat2, aspect = True)
        self.map.set_lonlat(geowindow)
        self.map.drawtiles(pixelwidth)

        self.maxpixwidth = 10000
        self.minpixwidth=100

        '''
        #pw.drawtiles(zoomlevel)
        pw.mark(my_location)
        pw.mark(loc2, color="blue", marker=".")
        '''

        but1=tk.Button(root, text="Resolution +", command=self.resolution_finer)
        but2=tk.Button(root, text="Resolution -", command=self.resolution_lesser)
        #but3=tk.Button(root, text="Jump location", command=self.jump_location)
        but4=tk.Button(root, text="Shift N", command=self.shift_N)
        but5=tk.Button(root, text="Shift S", command=self.shift_S)
        but6=tk.Button(root, text="Shift E", command=self.shift_E)
        but7=tk.Button(root, text="Shift W", command=self.shift_W)
        but8=tk.Button(root, text="Zoom In", command=self.zoom_in)
        but9=tk.Button(root, text="Zoom Out", command=self.zoom_out)
        myframe.pack()
        but1.pack(side=tk.LEFT)
        but2.pack(side=tk.LEFT)
        ##but3.pack(side=tk.LEFT)
        but4.pack(side=tk.LEFT)
        but5.pack(side=tk.LEFT)
        but6.pack(side=tk.LEFT)
        but7.pack(side=tk.LEFT)
        but8.pack(side=tk.LEFT)
        but9.pack(side=tk.LEFT)

    def resolution_finer(self):
        """ Increases pixel resolution """
        if self.map.pixelwidth < self.maxpixwidth:
            self.map.pixelwidth *=2
        self.map.redraw()
        print ("Actual pixel resolution: ",  self.map.pixelwidth) 
        print ("Actual zoom level: ",  self.map.zoomlevel) 

    def resolution_lesser(self):
        """ Decreases zoom level by 1"""
        if self.map.pixelwidth > self.minpixwidth:
            self.map.pixelwidth *=0.5
        self.map.redraw()    
        print ("Actual pixel resolution: ",  self.map.pixelwidth) 
        print ("Actual zoom level: ",  self.map.zoomlevel)     

    '''
    def jump_location(self):
        """ Deplaces marked location to the east"""
        x,y=pw.lastlocation
        x+=0.2
        pw.mark((x,y), clearold=True)
        #pw.mark((x,y))
     '''

    def shift_N(self):
        self.map.redraw_move(0.1, 0)

    def shift_S(self):
        self.map.redraw_move(-0.1, 0)

    def shift_E(self):
        self.map.redraw_move(0, 0.1)

    def shift_W(self):
        self.map.redraw_move(0, -0.1)

    def zoom_in(self):
        self.map.redraw_zoom(0.8)

    def zoom_out(self):
        self.map.redraw_zoom(1.2)

#----------------------------------------------------------------
if __name__ == '__main__':

    geowindow = getgeowindow(my_location, dlonlat)
    print(geowindow)

    root=tk.Tk()
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles = 'build_OSM()')
    # ALTERNATIVES:
    #       Attention: OSM and OSM_Humanitarian need functions!
    mw= MapFrame(root, size, pixelwidth, geowindow, tiles = 'build_OSM_Humanitarian()')
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Stamen_Toner')
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Stamen_Toner_Hybrid')
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Stamen_Terrain')
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Stamen_Watercolour')
    ##mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Carto_Dark')
    #mw= MapFrame(root, size, pixelwidth, geowindow, tiles='Carto_Light')

    #mw= MapFrame(root, size, pixelwidth, geowindow)                         # default is used

    root.mainloop()