Interactive (clickable) world map with wxpython

Hello Richard, thank you, your suggestion worked.

I’m trying to annotate the distance between 2 points. I calculate the distance, convert it to string, and I’m trying to annotate the distance in the midpoint of 2 coordinate points. But the annotation is placed too away from the red line. I would like it to be placed near the red line. Could you please have a look at the code below? Thank you.

Full code is below. "ne_110m_admin_0_countries.shp" map file can be downloaded from this page .You should put the sample code inside the map file directory.

import wx
import math
import pandas
from math import sin, cos, sqrt, atan2, radians
import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure
import geopandas as gpd
import random

this_directory = pathlib.Path(__file__).parent.resolve()

WORLDX  = os.path.join(this_directory, "ne_110m_admin_0_countries.shp")
WATER = '#defafc'
LAND = '#807e7e'
LEFT_MOUSE = matplotlib.backend_bases.MouseButton.LEFT


class MapFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Map",
                          style=wx.DEFAULT_FRAME_STYLE,
                          size=wx.Size(1200, 900))
        self.main_panel = MapPanel(self)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, _event):
        self.Destroy()
        wx.Exit()

class  MapPanel(wx.Panel):

    def __init__(self, parent):

        wx.Panel.__init__(self, parent)
        self.SetDoubleBuffered(True)


        self.figure = Figure()
        self.result_frame = parent
        self.canvas = FigureCanvas(self, -1, self.figure)
        self.axes = self.figure.add_axes([0, 0, 1, 1])
        self.axes.margins(0.0)
        self.axes.set_anchor('C')

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
        self.SetSizer(self.sizer)
        self.Fit()
        self.Layout()
        self.countries = ["US", "FR", "GB", "BR", "IN", "AU", "NZ", "IT", "DE"]
        self.gdf = None
        self.canvas.mpl_connect('button_press_event', self.onLeftClick)
        self.world_data = gpd.read_file(WORLDX)
        self.map_plot = None
        self.drawMap()

    def drawMap(self, country_highlight=None):
        """Draw the map on the canvas. """
        self.axes.clear()
        self.axes.axis('off')
        self.figure.set_facecolor(WATER)
        self.map_plot = self.world_data.to_crs(epsg=4326).plot(ax=self.axes, color=LAND)
        ax_aspect = self.map_plot.get_aspect()
        print("country highlight: ", country_highlight)
        if country_highlight:
            ylim = self.map_plot.get_ylim()
            xlim = self.map_plot.get_xlim()
            self.world_data[self.world_data.ISO_A2_EH == country_highlight].plot(edgecolor=u'gray', color='#fa8a48', ax=self.map_plot)
            self.map_plot.set_ylim(*ylim)
            self.map_plot.set_xlim(*xlim)

        self.map_plot.plot([-0.118092, -118.243683], [51.509865, 34.052235], linewidth=2, color="red", alpha=0.5)
        self.annotate_distance_between_two_locations(51.509865, -0.118092, 34.052235, -118.243683)
        self.map_plot.set_aspect(ax_aspect)
        self.canvas.draw()

    def annotate_distance_between_two_locations(self, lat1z, lon1z, lat2z, lon2z):

        mid_lat, mid_lon = self.get_midpoint(lat1z, lon1z, lat2z, lon2z)
        distance = int(self.get_distance(lat1z, lon1z, lat2z, lon2z))

        df = pandas.DataFrame(
            {
             'Distance': str(distance) + " km",
             'Latitude': [mid_lat],
             'Longitude': [mid_lon]})
        gdf = gpd.GeoDataFrame(
            df, geometry=gpd.points_from_xy(df.Longitude, df.Latitude))


        x_off = 0.1
        y_off = 0.1
        for x, y, distancex in zip(gdf.geometry.x, gdf.geometry.y, gdf["Distance"]):
            
            self.map_plot.annotate(distancex, xy=(x, y), xytext=(x_off, y_off),textcoords="offset pixels",
                                       size=8,
                                       color="red")



    def get_midpoint(self, lat1x, lon1x, lat2x, lon2x):
        lat1 = math.radians(lat1x)
        lon1 = math.radians(lon1x)
        lat2 = math.radians(lat2x)
        lon2 = math.radians(lon2x)

        bx = math.cos(lat2) * math.cos(lon2 - lon1)
        by = math.cos(lat2) * math.sin(lon2 - lon1)
        lat3 = math.atan2(math.sin(lat1) + math.sin(lat2), \
               math.sqrt((math.cos(lat1) + bx) * (math.cos(lat1) \
               + bx) + by**2))
        lon3 = lon1 + math.atan2(by, math.cos(lat1) + bx)

        return [round(math.degrees(lat3), 2), round(math.degrees(lon3), 2)]

    def get_distance(self, lat1, lon1, lat2, lon2):
        if not all(type(item) in [int, float] for item in [lat1, lon1, lat2, lon2]):
            return 999999
        R = 6371.2
        theta = lon2-lon1
        dista = math.acos(sin(lat1)*sin(lat2)+cos(lat1)*cos(lat2)*cos(theta))

        if dista < 0:
            dista = dista + math.pi

        dista = dista*6371.2

        return dista #kilometers

    def onLeftClick(self, event):
        if event.button == LEFT_MOUSE:
            self.drawMap(random.choice(self.countries))

class MyApp(wx.App):
    def OnInit(self):
        frame = MapFrame()
        frame.Show()
        return True

if __name__ == "__main__":
    app = MyApp()
    app.MainLoop()

Hi Steve,

I think your annotation is in the right place!

You calculated the distance on a great circle but drew it as a straight line on a flat earth.

I used an on-line great circle drawing tool (Great Circle Map) to show the shortest distance between London and LA:

London_to_LA

steves_map

1 Like

Hi Richard,

Thank you for the explanation. So I should use the plain midpoint formula then M = (x1+x2)/2, (y1+y2)/2

I wonder if it’s possible to draw a great circle with matplotlib.

Hello Richard,

I was able to place the distance string in the middle of the straight line by converting geographic coordinates to xyz coordinates and then calculating the midpoint. But there’s another problem. Since the earth is round, there are 2 possible midpoints, one of them belongs to East to West path, and the other is West to East path. So, the line is drawn from East to West path, but the distance string is placed on the middlepoint of other path (West to East). I hope you understood what I meant:

import wx
import math
import pandas
from math import sin, cos, sqrt, atan2, radians
import matplotlib
import matplotlib.backend_bases
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.figure import Figure
import geopandas as gpd
import pathlib
import os
import random
import numpy as np

this_directory = pathlib.Path(__file__).parent.resolve()

WORLDX = os.path.join(this_directory, "ne_110m_admin_0_countries.shp")

WATER = '#defafc'
LAND = '#807e7e'
LEFT_MOUSE = matplotlib.backend_bases.MouseButton.LEFT
RIGHT_MOUSE = matplotlib.backend_bases.MouseButton.RIGHT
MIDDLE_MOUSE = matplotlib.backend_bases.MouseButton.MIDDLE
MIN_ZOOM = 1
MAX_ZOOM = 16
LAND = 'tan'
WATER = '#e8efff'

def get_cartesian(lat=None,lon=None):
    lat, lon = np.deg2rad(lat), np.deg2rad(lon)
    R = 6371 # radius of the earth
    x = R * np.cos(lat) * np.cos(lon)
    y = R * np.cos(lat) * np.sin(lon)
    z = R *np.sin(lat)
    return x, y, z

def get_lat_lon_from_xyz(x, y, z):
    R = 6371
    lat = np.degrees(np.arcsin(z / R))
    lon = np.degrees(np.arctan2(y, x))
    return lat, lon

def get_midpoint_xyz(lat1x, lon1x, lat2x, lon2x):
    x1, y1, z1 = get_cartesian(lat1x, lon1x)
    x2, y2, z2 = get_cartesian(lat2x, lon2x)

    midx = (x1+x2)/2
    midy = (y1+y2)/2
    midz = (z1+z2)/2

    mlat, mlon = get_lat_lon_from_xyz(midx, midy, midz)

    print("x1, y1, z1: ", x1, y1, z1)
    print("x2, y2, z2: ", x2, y2, z2)
    print("midx, midy, midz: ", midx, midy, midz)
    print("lat1x, lon1x, lat2x, lon2x: ", lat1x, lon1x, lat2x, lon2x)
    print("mlat, mlon: ", mlat, mlon)
    return mlat, mlon

class MapFrame(wx.Frame):

    def __init__(self):
        wx.Frame.__init__(self, None, wx.ID_ANY, "Map",
                          style=wx.DEFAULT_FRAME_STYLE,
                          size=wx.Size(1200, 900))
        self.main_panel = MapPanel(self)
        self.Bind(wx.EVT_CLOSE, self.OnClose)

    def OnClose(self, _event):
        self.Destroy()
        wx.Exit()


class MapPanel(wx.Panel):

    def __init__(self, parent):

        wx.Panel.__init__(self, parent)
        self.SetDoubleBuffered(True)

        self.figure = Figure()
        self.result_frame = parent
        self.canvas = FigureCanvas(self, -1, self.figure)
        self.axes = self.figure.add_axes([0, 0, 1, 1])
        self.axes.margins(0.0)
        self.axes.set_anchor('C')

        self.sizer = wx.BoxSizer(wx.VERTICAL)
        self.sizer.Add(self.canvas, 1, wx.LEFT | wx.TOP | wx.GROW)
        self.SetSizer(self.sizer)
        self.Fit()
        self.Layout()
        self.countries = ["US", "FR", "GB", "BR", "IN", "AU", "NZ", "IT", "DE"]
        self.gdf = None
        self.canvas.mpl_connect('button_press_event', self.onLeftClick)
        self.world_data = gpd.read_file(WORLDX)
        self.map_plot = None
        self.drawMap()

    def drawMap(self, country_highlight=None):
        """Draw the map on the canvas. """
        self.axes.clear()
        self.axes.axis('off')
        self.figure.set_facecolor(WATER)
        self.map_plot = self.world_data.to_crs(epsg=4326).plot(ax=self.axes, color=LAND)
        ax_aspect = self.map_plot.get_aspect()
        print("country highlight: ", country_highlight)
        if country_highlight:
            ylim = self.map_plot.get_ylim()
            xlim = self.map_plot.get_xlim()
            self.world_data[self.world_data.ISO_A2_EH == country_highlight].plot(edgecolor=u'gray', color='#fa8a48', ax=self.map_plot)
            self.map_plot.set_ylim(*ylim)
            self.map_plot.set_xlim(*xlim)

        self.map_plot.plot([-46.473056, 151.177222], [-23.435556, -33.946111], linewidth=2, color="red", alpha=0.5)
        self.annotate_distance_between_two_locations(-33.946111, 151.177222, -23.435556, -46.473056)
        self.map_plot.set_aspect(ax_aspect)
        self.canvas.draw()

    def annotate_distance_between_two_locations(self, lat1z, lon1z, lat2z, lon2z):

        mid_lat, mid_lon = get_midpoint_xyz(lat1z, lon1z, lat2z, lon2z)
        distance = int(self.get_distance(lat1z, lon1z, lat2z, lon2z))

        df = pandas.DataFrame(
            {
                'Distance': str(distance) + " km",
                'Latitude': [mid_lat],
                'Longitude': [mid_lon]})
        gdf = gpd.GeoDataFrame(
            df, geometry=gpd.points_from_xy(df.Longitude, df.Latitude))

        x_off = 0.1
        y_off = 0.1
        for x, y, distancex in zip(gdf.geometry.x, gdf.geometry.y, gdf["Distance"]):
            self.map_plot.annotate(distancex, xy=(x, y), xytext=(x_off, y_off), textcoords="offset pixels",
                                   size=8,
                                   color="red")

    def get_midpoint(self, lat1x, lon1x, lat2x, lon2x):
        lat1 = math.radians(lat1x)
        lon1 = math.radians(lon1x)
        lat2 = math.radians(lat2x)
        lon2 = math.radians(lon2x)

        bx = math.cos(lat2) * math.cos(lon2 - lon1)
        by = math.cos(lat2) * math.sin(lon2 - lon1)
        lat3 = math.atan2(math.sin(lat1) + math.sin(lat2), \
                          math.sqrt((math.cos(lat1) + bx) * (math.cos(lat1) \
                                                             + bx) + by ** 2))
        lon3 = lon1 + math.atan2(by, math.cos(lat1) + bx)

        return [round(math.degrees(lat3), 2), round(math.degrees(lon3), 2)]

    def get_distance(self, lat1, lon1, lat2, lon2):
        if not all(type(item) in [int, float] for item in [lat1, lon1, lat2, lon2]):
            return 999999
        R = 6371.2
        theta = lon2 - lon1
        dista = math.acos(sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(theta))

        if dista < 0:
            dista = dista + math.pi

        dista = dista * 6371.2

        return dista  # kilometers

    def onLeftClick(self, event):
        if event.button == LEFT_MOUSE:
            self.drawMap(random.choice(self.countries))


class MyApp(wx.App):
    def OnInit(self):
        frame = MapFrame()
        frame.Show()
        return True


if __name__ == "__main__":
    app = MyApp()
    app.MainLoop()

Hi Steve,

I am afraid my knowledge of mathematics is currently very rusty, as the last course I did on the subject was 45 years ago! I’m sorry but can’t guarantee I will be able to help you solve this issue, plus I have a number of my own tasks I need to complete at the moment.

Not sure if it would be of any help but I did find the following page about plotting great circles using the plotly module:

There are other references on that page relating to using plotly on geopandas maps, but I don’t know if it would be possible to integrate it with your existing code that uses matplotlib.

Richard

1 Like

wx.html2 is better than wx.html, import the html file, and then run.
follium.py

import folium

m = folium.Map(location=[45.5236, -122.6750])

m.save("filename.html") 

wx.py

import wx
import wx.html2 as html2
class html_2(wx.Dialog):
    def __init__(self, *args, **kw):
        wx.Dialog.__init__(self,*args, **kw)
        box=wx.BoxSizer(wx.VERTICAL)
        self.browser=wx.html2.WebView.New(self)
        box.Add(self.browser,1,wx.EXPAND,10)
        self.SetSizer(box)
        self.SetSize((500,500))
if __name__=='__main__':
    app=wx.App()        
    dialog=html_2(None,-1)
    dialog.browser.LoadURL(r'path\filename.html')
    dialog.Show()
    app.MainLoop()