Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enables default basemap to be responsive to geometry type #239

Merged
merged 3 commits into from
Oct 10, 2017
Merged
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
78 changes: 60 additions & 18 deletions cartoframes/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -653,16 +653,6 @@ def map(self, layers=None, interactive=True,
[zoom, lat, lng] = [3, 38, -99]
has_zoom = zoom is not None

# Check basemaps, add one if none exist
base_layers = [idx for idx, layer in enumerate(layers)
if layer.is_basemap]
if len(base_layers) > 1:
raise ValueError('map can at most take 1 BaseMap layer')
if len(base_layers) > 0:
layers.insert(0, layers.pop(base_layers[0]))
else:
layers.insert(0, BaseMap())

# Check for a time layer, if it exists move it to the front
time_layers = [idx for idx, layer in enumerate(layers)
if not layer.is_basemap and layer.time]
Expand All @@ -677,27 +667,51 @@ def map(self, layers=None, interactive=True,
'time_column')
layers.append(layers.pop(time_layers[0]))

# If basemap labels are on front, add labels layer
basemap = layers[0]
if basemap.is_basic() and basemap.labels == 'front':
layers.append(BaseMap(basemap.source,
labels=basemap.labels,
only_labels=True))
base_layers = [idx for idx, layer in enumerate(layers)
if layer.is_basemap]

# Check basemaps, add one if none exist
if len(base_layers) > 1:
raise ValueError('Map can at most take one BaseMap layer')
elif len(base_layers) == 1:
layers.insert(0, layers.pop(base_layers[0]))
elif not base_layers:
# default basemap is dark with labels in back
# labels will be changed if all geoms are non-point
layers.insert(0, BaseMap(source='dark', labels='back'))
geoms = set()

# Setup layers
for idx, layer in enumerate(layers):
if not layer.is_basemap:
# get schema of style columns
resp = self.sql_client.send('''
SELECT {cols} FROM ({query}) AS _wrap LIMIT 0
SELECT {cols}
FROM ({query}) AS _wrap
LIMIT 0
'''.format(cols=','.join(layer.style_cols),
query=layer.query))
self._debug_print(layer_fields=resp)
# update local style schema to help build proper defaults
for k, v in dict_items(resp['fields']):
layer.style_cols[k] = v['type']
if not base_layers:
layer.geom_type = self._geom_type(layer)
geoms.add(layer.geom_type)
# update local style schema to help build proper defaults
layer._setup(layers, idx)

# set labels on top if there are no point geometries and a basemap
# is not specified
if not base_layers and 'point' not in geoms:
layers[0] = BaseMap(labels='front')

# If basemap labels are on front, add labels layer
basemap = layers[0]
if basemap.is_basic() and basemap.labels == 'front':
layers.append(BaseMap(basemap.source,
labels=basemap.labels,
only_labels=True))

nb_layers = non_basemap_layers(layers)
options = {'basemap_url': basemap.url}

Expand Down Expand Up @@ -822,6 +836,34 @@ def safe_quotes(text, escape_single_quotes=False):
height=size[1],
metadata=dict(origin_url=static_url))

def _geom_type(self, layer):
"""gets geometry type(s) of specified layer"""
resp = self.sql_client.send('''
SELECT
CASE WHEN ST_GeometryType(the_geom) in ('ST_Point',
'ST_MultiPoint')
THEN 'point'
WHEN ST_GeometryType(the_geom) in ('ST_LineString',
'ST_MultiLineString')
THEN 'line'
WHEN ST_GeometryType(the_geom) in ('ST_Polygon',
'ST_MultiPolygon')
THEN 'polygon'
ELSE null END AS geom_type,
count(*) as cnt
FROM ({query}) AS _wrap
WHERE the_geom IS NOT NULL
GROUP BY 1
ORDER BY 2 DESC
'''.format(query=layer.query))
if len(resp['rows']) > 1:
warn('There are multiple geometry types in {query}: '
'{geoms}. Styling by `{common_geom}`, the most common'.format(
query=layer.query,
geoms=','.join(g['geom_type'] for g in resp['rows']),
common_geom=resp['rows'][0]['geom_type']))
return resp['rows'][0]['geom_type']

def data_boundaries(self, df=None, table_name=None):
"""Not currently implemented"""
pass
Expand Down
1 change: 1 addition & 0 deletions cartoframes/layer.py
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ def __init__(self, query, time=None, color=None, size=None,
self.query = query
# style columns as keys, data types as values
self.style_cols = dict()
self.geom_type = None

# TODO: move these if/else branches to individual methods
# color, scheme = self._get_colorscheme()
Expand Down
49 changes: 49 additions & 0 deletions test/test_context.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ def setUp(self):
self.valid_columns = set(['affgeoid', 'aland', 'awater', 'created_at',
'csafp', 'geoid', 'lsad', 'name', 'the_geom',
'updated_at'])
self.test_point_table = 'tweets_obama'

# for writing to carto
self.test_write_table = 'cartoframes_test_table_{ver}_{mpl}'.format(
ver=pyver,
Expand Down Expand Up @@ -505,6 +507,53 @@ def test_cartocontext_map(self):
with self.assertRaises(NotImplementedError):
cc.map(layers=Layer(self.test_read_table, time='cartodb_id'))

@unittest.skipIf(WILL_SKIP, 'no carto credentials, skipping this test')
def test_cartocontext_map_geom_type(self):
"""CartoContext.map basemap geometry type defaults"""
from cartoframes import Layer, QueryLayer
cc = cartoframes.CartoContext(base_url=self.baseurl,
api_key=self.apikey)

# baseid1 = dark, labels1 = labels on top in named map name
labels_polygon = cc.map(layers=Layer(self.test_read_table))
self.assertRegexpMatches(labels_polygon.__html__(),
'.*baseid1_labels1.*',
msg='labels should be on top since only a '
'polygon layer is present')

# baseid1 = dark, labels0 = labels on bottom
labels_point = cc.map(layers=Layer(self.test_point_table))
self.assertRegexpMatches(labels_point.__html__(),
'.*baseid1_labels0.*',
msg='labels should be on bottom because a '
'point layer is present')

labels_multi = cc.map(layers=[Layer(self.test_point_table),
Layer(self.test_read_table)])
self.assertRegexpMatches(labels_multi.__html__(),
'.*baseid1_labels0.*',
msg='labels should be on bottom because a '
'point layer is present')
# create a layer with points and polys, but with more polys
# should default to poly layer (labels on top)
multi_geom_layer = QueryLayer('''
(SELECT
the_geom, the_geom_webmercator,
row_number() OVER () AS cartodb_id
FROM "{polys}" WHERE the_geom IS NOT null LIMIT 10)
UNION ALL
(SELECT
the_geom, the_geom_webmercator,
(row_number() OVER ()) + 10 AS cartodb_id
FROM "{points}" WHERE the_geom IS NOT null LIMIT 5)
'''.format(polys=self.test_read_table,
points=self.test_point_table))
multi_geom = cc.map(layers=multi_geom_layer)
self.assertRegexpMatches(multi_geom.__html__(),
'.*baseid1_labels1.*',
msg='layer has more polys than points, so it '
'should default to polys labels (on top)')

@unittest.skipIf(WILL_SKIP, 'no carto credentials, skipping')
def test_get_bounds(self):
"""CartoContext._get_bounds"""
Expand Down