diff --git a/mapboxgl/templates/linestring.html b/mapboxgl/templates/linestring.html index 3a6c1df..c654ec9 100644 --- a/mapboxgl/templates/linestring.html +++ b/mapboxgl/templates/linestring.html @@ -3,14 +3,25 @@ {% block extra_css %} {% endblock extra_css %} {% block legend %} var legend = document.getElementById('legend'); - calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + + {% if colorStops and colorProperty and widthProperty %} + {% if colorProperty != widthProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }} vs. {{ widthProperty }}"); + {% else %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% endif %} + {% elif colorStops and colorProperty %} + calcCircleColorLegend({{ colorStops }}, "{{ colorProperty }}"); + {% else %} + document.getElementById('legend').style.visibility='hidden'; + {% endif %} {% endblock legend %} @@ -39,10 +50,18 @@ }, "paint": { {% if lineDashArray %} - "line-dasharray": {{ lineDashArray }}, + "line-dasharray": {{ lineDashArray }}, + {% endif %} + {% if colorProperty %} + "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), + {% else %} + "line-color": "{{ defaultColor }}", + {% endif %} + {% if widthProperty %} + "line-width": generatePropertyExpression("{{ widthType }}", "{{ widthProperty }}", {{ widthStops }}, "{{ defaultWidth }}"), + {% else %} + "line-width": {{ defaultWidth }}, {% endif %} - "line-color": generatePropertyExpression("{{ colorType }}", "{{ colorProperty }}", {{ colorStops }}, "{{ defaultColor }}"), - "line-width": {{ lineWidth }}, "line-opacity": {{ opacity }} } }, "{{ belowLayer }}" ); diff --git a/mapboxgl/templates/vector_linestring.html b/mapboxgl/templates/vector_linestring.html index 737e6ec..55da9de 100644 --- a/mapboxgl/templates/vector_linestring.html +++ b/mapboxgl/templates/vector_linestring.html @@ -6,13 +6,24 @@ {% if joinData %} let joinData = {{ joinData }}; - var popUpKeys = {}; + var popUpKeys = {}, + lineWidthPopUpKeys = {}; // Create filter for layers from join data - let layerFilter = ['in', "{{ vectorJoinColorProperty }}"] + let layerFilter = ['in', "{{ vectorJoinDataProperty }}"] joinData.forEach(function(row, index) { - popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + + {% if colorProperty %} + popUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ colorProperty }}"]; + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + lineWidthPopUpKeys[row["{{ dataJoinProperty }}"]] = row["{{ widthProperty }}"]; + {% endif %} + {% endif %} + layerFilter.push(row["{{ dataJoinProperty }}"]); }); @@ -40,11 +51,17 @@ {% endif %} "line-color": { "type": "categorical", - "property": "{{ vectorJoinColorProperty }}", + "property": "{{ vectorJoinDataProperty }}", "stops": {{ vectorColorStops }}, "default": "{{ defaultColor }}" }, - "line-width": {{ lineWidth }}, + "line-width": { + "type": "categorical", + "property": "{{ vectorJoinDataProperty }}", + "stops": {{ vectorWidthStops }}, + "default": {{ defaultWidth }} + }, + "line-opacity": {{ opacity }} }, filter: layerFilter @@ -86,7 +103,15 @@ } // Add property from joined data to vector's feature popup - popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinColorProperty }}"]] + '
  • ' + {% if colorProperty %} + popup_html += '
  • ' + "{{ colorProperty }}".toUpperCase() + ': ' + popUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + + {% if widthProperty %} + {% if colorProperty != widthProperty %} + popup_html += '
  • ' + "{{ widthProperty }}".toUpperCase() + ': ' + lineWidthPopUpKeys[f.properties["{{ vectorJoinDataProperty }}"]] + '
  • ' + {% endif %} + {% endif %} popup_html += '' popup.setLngLat(e.lngLat) diff --git a/mapboxgl/viz.py b/mapboxgl/viz.py index 7163f00..cf0ae56 100644 --- a/mapboxgl/viz.py +++ b/mapboxgl/viz.py @@ -13,6 +13,67 @@ GL_JS_VERSION = 'v0.44.1' +def height_map(lookup, height_stops, default_height=0.0): + """Return a height value (in meters) interpolated from given height_stops; + for use with vector-based visualizations using fill-extrusion layers + """ + # if no height_stops, use default height + if len(height_stops) == 0: + return default_height + + # dictionary to lookup height from match-type height_stops + match_map = dict((x, y) for (x, y) in height_stops) + + # if lookup matches stop exactly, return corresponding height (first priority) + # (includes non-numeric height_stop "keys" for finding height by match) + if lookup in match_map.keys(): + return match_map.get(lookup) + + # if lookup value numeric, map height by interpolating from height scale + if isinstance(lookup, (int, float, complex)): + + # try ordering stops + try: + stops, heights = zip(*sorted(height_stops)) + + # if not all stops are numeric, attempt looking up as if categorical stops + except TypeError: + return match_map.get(lookup, default_height) + + # for interpolation, all stops must be numeric + if not all(isinstance(x, (int, float, complex)) for x in stops): + return default_height + + # check if lookup value in stops bounds + if float(lookup) <= stops[0]: + return heights[0] + + elif float(lookup) >= stops[-1]: + return heights[-1] + + # check if lookup value matches any stop value + elif float(lookup) in stops: + return heights[stops.index(lookup)] + + # interpolation required + else: + + # identify bounding height stop values + lower = max([stops[0]] + [x for x in stops if x < lookup]) + upper = min([stops[-1]] + [x for x in stops if x > lookup]) + + # heights from bounding stops + lower_height = heights[stops.index(lower)] + upper_height = heights[stops.index(upper)] + + # compute linear "relative distance" from lower bound height to upper bound height + distance = (lookup - lower) / (upper - lower) + + # return string representing rgb height value + return lower_height + distance * (upper_height - lower_height) + + # default height value catch-all + return default_height class MapViz(object): @@ -555,7 +616,6 @@ def add_unique_template_variables(self, options): tiles_bounds=self.tiles_bounds if self.tiles_bounds else 'undefined')) - class LinestringViz(MapViz): """Create a linestring viz""" @@ -564,32 +624,36 @@ def __init__(self, vector_url=None, vector_layer_name=None, vector_join_property=None, - data_join_property=None, # vector only + data_join_property=None, label_property=None, color_property=None, color_stops=None, color_default='grey', color_function_type='interpolate', - line_color='white', line_stroke='solid', - line_width=1, + line_width_property=None, + line_width_stops=None, + line_width_default=1, + line_width_function_type='interpolate', *args, **kwargs): """Construct a Mapviz object :param data: can be either GeoJSON (containing polygon features) or JSON for data-join technique with vector polygons - :param vector_url: optional property to define vector polygon source + :param vector_url: optional property to define vector linestring source :param vector_layer_name: property to define target layer of vector source - :param vector_join_property: property to aid in determining color for styling vector polygons + :param vector_join_property: property to aid in determining color for styling vector lines :param data_join_property: property to join json data to vector features :param label_property: property to use for marker label - :param color_property: property to determine circle color - :param color_stops: property to determine circle color - :param color_default: property to determine default circle color if match lookup fails + :param color_property: property to determine line color + :param color_stops: property to determine line color + :param color_default: property to determine default line color if match lookup fails :param color_function_type: property to determine `type` used by Mapbox to assign color - :param line_color: property to determine choropleth line color - :param line_stroke: property to determine choropleth line stroke (solid, dashed, dotted, dash dot) - :param line_width: property to determine choropleth line width + :param line_stroke: property to determine line stroke (solid, dashed, dotted, dash dot) + :param line_width_property: property to determine line width + :param line_width_stops: property to determine line width + :param line_width_default: property to determine default line width if match lookup fails + :param line_width_function_type: property to determine `type` used by Mapbox to assign line width """ super(LinestringViz, self).__init__(data, *args, **kwargs) @@ -611,9 +675,11 @@ def __init__(self, self.color_stops = color_stops self.color_default = color_default self.color_function_type = color_function_type - self.line_color = line_color self.line_stroke = line_stroke - self.line_width = line_width + self.line_width_property = line_width_property + self.line_width_stops = line_width_stops + self.line_width_default = line_width_default + self.line_width_function_type = line_width_function_type def generate_vector_color_map(self): """Generate color stops array for use with match expression in mapbox template""" @@ -628,8 +694,25 @@ def generate_vector_color_map(self): return vector_stops + def generate_vector_width_map(self): + """Generate width stops array for use with match expression in mapbox template""" + vector_stops = [] + + if self.line_width_function_type == 'match': + match_width = self.line_width_stops + + for row in self.data: + + # map width to JSON feature using width_property + width = height_map(row[self.line_width_property], self.line_width_stops, self.line_width_default) + + # link to vector feature using data_join_property (from JSON object) + vector_stops.append([row[self.data_join_property], width]) + + return vector_stops + def add_unique_template_variables(self, options): - """Update map template variables specific to heatmap visual""" + """Update map template variables specific to linestring visual""" # set line stroke dash interval based on line_stroke property if self.line_stroke in ["dashed", "--"]: @@ -644,30 +727,40 @@ def add_unique_template_variables(self, options): # default to solid line self.line_dash_array = [1, 0] - # common variables for vector and geojson-based choropleths + # common variables for vector and geojson-based linestring maps options.update(dict( colorStops=self.color_stops, colorProperty=self.color_property, colorType=self.color_function_type, defaultColor=self.color_default, - lineColor=self.line_color, + lineColor=self.color_default, lineDashArray=self.line_dash_array, lineStroke=self.line_stroke, - lineWidth=self.line_width, + widthStops=self.line_width_stops, + widthProperty=self.line_width_property, + widthType=self.line_width_function_type, + defaultWidth=self.line_width_default, )) - # vector-based choropleth map variables + # vector-based linestring map variables if self.vector_source: options.update(dict( vectorUrl=self.vector_url, vectorLayer=self.vector_layer_name, - vectorColorStops=self.generate_vector_color_map(), - vectorJoinColorProperty=self.vector_join_property, + vectorJoinDataProperty=self.vector_join_property, + vectorColorStops=[[0,self.color_default]], + vectorWidthStops=[[0,self.line_width_default]], joinData=json.dumps(self.data, ensure_ascii=False), dataJoinProperty=self.data_join_property, )) + + if self.color_property: + options.update(dict(vectorColorStops=self.generate_vector_color_map())) - # geojson-based choropleth map variables + if self.line_width_property: + options.update(dict(vectorWidthStops=self.generate_vector_width_map())) + + # geojson-based linestring map variables else: options.update(dict( geojson_data=json.dumps(self.data, ensure_ascii=False),