Skip to content

Commit

Permalink
Merge pull request #239 from CartoDB/iss186-basemap-response-to-geometry
Browse files Browse the repository at this point in the history
enables default basemap to be responsive to geometry type
  • Loading branch information
andy-esch authored Oct 10, 2017
2 parents 49cfc1d + cf72275 commit 3ccbd40
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 18 deletions.
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 @@ -507,6 +509,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

0 comments on commit 3ccbd40

Please sign in to comment.