Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 128 additions & 114 deletions mapio/basemapcity.py
Original file line number Diff line number Diff line change
@@ -1,28 +1,27 @@
#stdlib imports
import os.path
import warnings

#local imports
# local imports
from .mapcity import MapCities
from .dataset import DataSetException,DataSetWarning
from .dataset import DataSetException

# third party imports

#third party imports
import matplotlib.font_manager
import matplotlib.patheffects as path_effects
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd

XOFFSET = 4 #how many pixels between the city dot and the city text
XOFFSET = 4 # how many pixels between the city dot and the city text


class BasemapCities(MapCities):
"""
A subclass of Cities that can remove cities whose labels on a map
intersect with larger cities.

"""

def limitByMapCollision(self,basemap,fontname='Bitstream Vera Sans',fontsize=10.0):
"""

def limitByMapCollision(
self, basemap, fontname="Bitstream Vera Sans", fontsize=10.0
):
"""Create a smaller Cities dataset by removing smaller cities whose bounding
boxes collide with larger cities.

Expand All @@ -39,7 +38,6 @@ def limitByMapCollision(self,basemap,fontname='Bitstream Vera Sans',fontsize=10.
resolution='l',area_thresh=1000.,projection='lcc',\
lat_1=50.,lon_0=-107.,ax=ax)
#######################################

:param fontsize:
Desired font size for city labels.
:param fontname:
Expand All @@ -55,46 +53,41 @@ def limitByMapCollision(self,basemap,fontname='Bitstream Vera Sans',fontsize=10.
New Cities instance where smaller colliding cities have been removed.
"""
if basemap.ax is None:
raise DataSetException('Basemap is missing a reference to an "axes" object.')
raise DataSetException(
'Basemap is missing a reference to an "axes" object.'
)
ax = basemap.ax

#get the transformation from display units (pixels) to data units
#we'll use this to set an offset in pixels between the city dot and
#the city name.
# get the transformation from display units (pixels) to data units
# we'll use this to set an offset in pixels between the city dot and
# the city name.
self._display_to_data_transform = ax.transData.inverted()

self.project(basemap)
if 'x' not in self._dataframe.columns and 'y' not in self._dataframe.columns:
raise DataSetException('Cities object has not had project() called yet.')
if "x" not in self._dataframe.columns and "y" not in self._dataframe.columns:
raise DataSetException("Cities object has not had project() called yet.")

if fontname not in self._fontlist:
raise DataSetException('Font %s not in supported list.' % fontname)
#TODO: - check placement column
raise DataSetException("Font %s not in supported list." % fontname)
# TODO: - check placement column
newdf = self._dataframe.copy()
#older versions of pandas use a different sort function
if pd.__version__ < '0.17.0':
newdf = newdf.sort(columns='pop',ascending=False)
# older versions of pandas use a different sort function
if pd.__version__ < "0.17.0":
newdf = newdf.sort(columns="pop", ascending=False)
else:
newdf = newdf.sort_values(by='pop',ascending=False)
newdf = newdf.sort_values(by="pop", ascending=False)

lefts,rights,bottoms,tops = self._getCityBoundingBoxes(newdf,fontname,fontsize,ax)
lefts, rights, bottoms, tops = self._getCityBoundingBoxes(
newdf, fontname, fontsize, ax
)
ikeep = ~np.isnan(lefts)
newdf = newdf.iloc[ikeep]
lefts = lefts[ikeep]
rights = rights[ikeep]
bottoms = bottoms[ikeep]
tops = tops[ikeep]
ikeep = [0] #indices of rows to keep in dataframe
for i in range(1,len(tops)):
cname = newdf.iloc[i]['name']
if cname.lower().find('pacific grove') > -1:
allnames = newdf['name'].tolist()
sidx = allnames.index('Salinas')
sleft = lefts[sidx]
sright = rights[sidx]
sbottom = bottoms[sidx]
stop = tops[sidx]
foo = 1
ikeep = [0] # indices of rows to keep in dataframe
for i in range(1, len(tops)):
if np.isnan(lefts[i]):
continue
left = lefts[i]
Expand All @@ -104,23 +97,23 @@ def limitByMapCollision(self,basemap,fontname='Bitstream Vera Sans',fontsize=10.

clrx = (left > rights[0:i]) | (right < lefts[0:i])
clry = (top < bottoms[0:i]) | (bottom > tops[0:i])
allclr = (clrx | clry)
allclr = clrx | clry
if all(allclr):
ikeep.append(i)
else:
foo = 1
if len(newdf):
try:
newdf = newdf.iloc[ikeep]
newdf['top'] = tops[ikeep]
newdf['bottom'] = bottoms[ikeep]
newdf['left'] = lefts[ikeep]
newdf['right'] = rights[ikeep]
newdf["top"] = tops[ikeep]
newdf["bottom"] = bottoms[ikeep]
newdf["left"] = lefts[ikeep]
newdf["right"] = rights[ikeep]
except Exception as e:
x = 1
print(e)
return type(self)(newdf)

def renderToMap(self,ax,fontname='Bitstream Vera Sans',fontsize=10.0,zorder=10,shadow=False):
def renderToMap(
self, ax, fontname="Bitstream Vera Sans", fontsize=10.0, zorder=10, shadow=False
):
"""Render cities on Basemap axes.

:param ax:
Expand All @@ -130,23 +123,21 @@ def renderToMap(self,ax,fontname='Bitstream Vera Sans',fontsize=10.0,zorder=10,s
:param fontsize:
Font size in points.
:param zorder:
Matplotlib plotting order - higher zorder is on top.
Matplotlib plotting order - higher zorder is on top.
:param shadow:
Boolean indicating whether "drop-shadow" effect should be used.
"""
if 'x' not in self._dataframe.columns and 'y' not in self._dataframe.columns:
raise DataSetException('Cities object has not had project() called yet.')
#get the transformation from display units (pixels) to data units
#we'll use this to set an offset in pixels between the city dot and
#the city name.
if "x" not in self._dataframe.columns and "y" not in self._dataframe.columns:
raise DataSetException("Cities object has not had project() called yet.")
# get the transformation from display units (pixels) to data units
# we'll use this to set an offset in pixels between the city dot and
# the city name.
self._display_to_data_transform = ax.transData.inverted()

for index,row in self._dataframe.iterrows():
th = self._renderRow(row,ax,fontname,fontsize,shadow=shadow,
zorder=zorder)
ax.plot(row['x'],row['y'],'k.')

def _getCityBoundingBoxes(self,df,fontname,fontsize,ax):

for index, row in self._dataframe.iterrows():
ax.plot(row["x"], row["y"], "k.")

def _getCityBoundingBoxes(self, df, fontname, fontsize, ax):
"""Get the axes coordinate system bounding boxes for each city.
:param df:
DataFrame containing information about cities.
Expand All @@ -160,42 +151,46 @@ def _getCityBoundingBoxes(self,df,fontname,fontsize,ax):
Numpy arrays of top,bottom,left and right edges of city bounding boxes.
"""
fig = ax.get_figure()
fwidth,fheight = fig.get_figwidth(),fig.get_figheight()
fwidth, fheight = fig.get_figwidth(), fig.get_figheight()
plt.sca(ax)
axmin,axmax,aymin,aymax = plt.axis()
axmin, axmax, aymin, aymax = plt.axis()
axbox = ax.get_position().bounds
newfig = plt.figure(figsize=(fwidth,fheight))
newfig = plt.figure(figsize=(fwidth, fheight))
newax = newfig.add_axes(axbox)
newfig.canvas.draw()
plt.sca(newax)
plt.axis((axmin,axmax,aymin,aymax))
#make arrays of the edges of all the bounding boxes
tops = np.ones(len(df))*np.nan
bottoms = np.ones(len(df))*np.nan
lefts = np.ones(len(df))*np.nan
rights = np.ones(len(df))*np.nan
plt.axis((axmin, axmax, aymin, aymax))
# make arrays of the edges of all the bounding boxes
tops = np.ones(len(df)) * np.nan
bottoms = np.ones(len(df)) * np.nan
lefts = np.ones(len(df)) * np.nan
rights = np.ones(len(df)) * np.nan
if len(df):
left,right,bottom,top = self._getCityEdges(df.iloc[0],newax,newfig,fontname,fontsize)
left, right, bottom, top = self._getCityEdges(
df.iloc[0], newax, newfig, fontname, fontsize
)
lefts[0] = left
rights[0] = right
bottoms[0] = bottom
tops[0] = top
for i in range(1,len(df)):
for i in range(1, len(df)):
row = df.iloc[i]
left,right,bottom,top = self._getCityEdges(row,newax,newfig,fontname,fontsize)
#remove cities that have any portion off the map
left, right, bottom, top = self._getCityEdges(
row, newax, newfig, fontname, fontsize
)
# remove cities that have any portion off the map
if left < axmin or right > axmax or bottom < aymin or top > aymax:
continue
lefts[i] = left
rights[i] = right
bottoms[i] = bottom
tops[i] = top

#get rid of new figure and its axes
# get rid of new figure and its axes
plt.close(newfig)
return (lefts,rights,bottoms,tops)
return (lefts, rights, bottoms, tops)

def _renderRow(self,row,ax,fontname,fontsize,zorder=10,shadow=False):
def _renderRow(self, row, ax, fontname, fontsize, zorder=10, shadow=False):
"""Internal method to consistently render city names.
:param row:
pandas dataframe row.
Expand All @@ -206,48 +201,70 @@ def _renderRow(self,row,ax,fontname,fontsize,zorder=10,shadow=False):
:param fontsize:
Font size in points.
:param zorder:
Matplotlib plotting order - higher zorder is on top.
Matplotlib plotting order - higher zorder is on top.
:param shadow:
Boolean indicating whether "drop-shadow" effect should be used.
:returns:
Matplotlib Text instance.
"""
ha = 'left'
va = 'center'
if 'placement' in row.index:
if row['placement'].find('E') > -1:
ha = 'left'
if row['placement'].find('W') > -1:
ha = 'right'
ha = "left"
va = "center"
if "placement" in row.index:
if row["placement"].find("E") > -1:
ha = "left"
if row["placement"].find("W") > -1:
ha = "right"
else:
ha = 'center'
if row['placement'].find('N') > -1:
ha = 'top'
if row['placement'].find('S') > -1:
ha = 'bottom'
ha = "center"
if row["placement"].find("N") > -1:
ha = "top"
if row["placement"].find("S") > -1:
ha = "bottom"
else:
ha = 'center'
display1 = (1,1)
display2 = (1+XOFFSET,1)
ha = "center"

display1 = (1, 1)
display2 = (1 + XOFFSET, 1)
data1 = self._display_to_data_transform.transform((display1))
data2 = self._display_to_data_transform.transform((display2))
data_x_offset = data2[0] - data1[0]
tx = row['x'] + data_x_offset
ty = row['y']
if shadow:
th = ax.text(tx,ty,row['name'],fontname=fontname,color='white',
fontsize=fontsize,ha=ha,va=va,zorder=zorder,clip_on=True)
th.set_path_effects([path_effects.Stroke(linewidth=1.5, foreground='black'),
path_effects.Normal()])
else:
th = ax.text(tx,ty,row['name'],fontname=fontname,
fontsize=fontsize,ha=ha,va=va,zorder=zorder,clip_on=True)

tx = row["x"] + data_x_offset
ty = row["y"]
if shadow:
th = ax.text(
tx,
ty,
row["name"],
fontname=fontname,
color="white",
fontsize=fontsize,
ha=ha,
va=va,
zorder=zorder,
clip_on=True,
)
th.set_path_effects(
[
path_effects.Stroke(linewidth=1.5, foreground="black"),
path_effects.Normal(),
]
)
else:
th = ax.text(
tx,
ty,
row["name"],
fontname=fontname,
fontsize=fontsize,
ha=ha,
va=va,
zorder=zorder,
clip_on=True,
)

return th


def _getCityEdges(self,row,ax,fig,fontname,fontsize):

def _getCityEdges(self, row, ax, fig, fontname, fontsize):
"""Return the edges of a city label on a given map axes.
:param row:
Row of a dataframe containing city information.
Expand All @@ -260,11 +277,8 @@ def _getCityEdges(self,row,ax,fig,fontname,fontsize):
:param fontsize:
Font size in points.
"""
th = self._renderRow(row,ax,fontname,fontsize)
th = self._renderRow(row, ax, fontname, fontsize)
bbox = th.get_window_extent(fig.canvas.renderer)
axbox = bbox.inverse_transformed(ax.transData)
left,bottom,right,top = axbox.extents
return (left,right,bottom,top)



left, bottom, right, top = axbox.extents
return (left, right, bottom, top)
Loading