Commit 9b4e34e5 authored by S Anand's avatar S Anand

ENH: mapviewer addNote, add/remove layer, show mismatches

Fixes #112
Fixes #118
By @bharat.r
parent f36ecaa0
Pipeline #72945 passed with stage
in 2 minutes and 18 seconds
......@@ -27,6 +27,160 @@ This creates a simple base map:
</script>
```
This loads a [GeoJSON file](test/india-states.geojson), links data from
[state_score.json](test/state_score.json), and sets the fill color from a merged
attribute.
```html
<div id="geojson-map" style="height:300px">
<script>
var map = g1.mapviewer({
id: 'geojson-map',
layers: {
worldMap: { type: 'tile', url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
indiaGeojson: {
type: 'geojson',
url: 'india-states.geojson',
link: {
url: 'state_score.json', // Load data from this file
dataKey: 'name', // Join this column from the URL (data)
mapKey: 'ST_NM' // with this property in the GeoJSON
},
options: {
style: {
fillColor: '#a00',
fillOpacity: 1
}
},
tooltip: function(prop) { // On hover, show this HTML tooltip
return prop.ST_NM + ': ' + prop.TOT_P
},
attrs: {
fillColor: { // Fill the regions
metric: 'score', // with the "score" column from state_score.json
scheme: 'RdYlGn' // using a RdYlGn gradient
}
}
}
}
})
</script>
```
**Note**: You can use `type: 'topojson'` when loading TopoJSON maps.
## g1.mapviewer options
- `id`: ID of the map DOM element (example: `mapid`), or the DOM element
- `map`:
- `options`: supports same options as [Map options](http://leafletjs.com/reference-1.3.0.html#map)
- `layers`: builds layers one on top of another in the specified order.
- `{layername: {...} }` dict with layer name as keys
- Each layer MUST have a type. Currently supported types are
- tile
- geojson
- topojson
- marker (`link`: option is not yet supported )
- circleMarker
- `tile` layer MUST have a url: that has the URL template for the leaflet tile layer.
- `url`: A string of the form - `http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png`
- `options`: supports same options as [Tile options](http://leafletjs.com/reference-1.3.0.html#tilelayer-minzoom)
- `geojson` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `options`: supports same options as [GeoJSON options](http://leafletjs.com/reference-1.3.0.html#geojson-style)
- `link`: adds attributes from input dataset to geojson/topojson
- `url`: url (String) to fetch data
- `mapKey`: attribute name in geojson/topojson to match
- `dataKey`: column name in input dataset that matches with geojson/topojson `mapKey`
- `mismatch`: `true` (default) / `false` / function. Displays the number of mismatches between `dataKey` and `mapKey`. By default, appears as a label at the bottom left corner of the map. To customise the message, use function
- function accepts an list of objects. Each object has `status` and all geojson/topojson feature properties:
- `status`: A boolean value representing whether the geojson/topojson feature found a match from input dataset or not.
- `popup`: string / function that returns formatted value
- `popupOptions`: An object with properties and values from [leaflet popup options](https://leafletjs.com/reference-1.3.4.html#popup-l-popup)
- `tooltip`: string / function that returns formatted value.
- function(properties) must return a string. feature properties are passed as argument.
- `tooltipOptions`: An object with properties and values from [leaflet tooltip options](https://leafletjs.com/reference-1.3.0.html#tooltip-option)
- `direction` property can be a string or function. function is passed the following arguments.
- `centerPoint` is center coordinates of map view
- `tooltipPoint` is center coordinates of tooltip
- `properties` are feature properties
NOTE: `tooltip` and `tooltipOptions` are previously (till v0.9.l) are inside `attrs`. This spec breaking change is mage to maintain consistency with `popup` and `popupOptions`
- `attrs` Data driven styles. same as `options`. (`attrs` take priority over `options`)
- For `color`, `weight`, `opacity`, `fillColor`, `fillOpacity` properties, the options are:.
- `metric` string / function
- If `metric`: is a string, can be any numeric property of geojson
- To have a metric that is formala based on multiple properties, use function. Example: `function(row) { return row['congress_votes'] - row['bjp_votes']}`
- `domain` An array of two numbers. Defaults to calculated values of given `metric`.
- `range`
- For `fillColor` and `color`, must be a [interpolate color scheme](https://github.com/d3/d3-scale-chromatic#diverging)
- `topojson` - same as `geojson`
- `marker` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `latitude`: String (mandatory). Must be column name that contains latitude of marker
- `longitude`: String (mandatory). Must be column name that contains longitude of marker
- `options`: supports same options as [marker options](http://leafletjs.com/reference-1.3.0.html#marker-icon)
- `circleMarker` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `latitude`: String (mandatory). Must be column name that contains latitude of marker
- `longitude`: String (mandatory). Must be column name that contains longitude of marker
- `options`: supports same options as [circleMarker options](http://leafletjs.com/reference-1.3.0.html#circlemarker-radius)
- `attrs` same as `attrs` for `geojson` type layer
- `drilldown`:
- `rootLayer`: `geojson/topojson` layer that acts as root layer to drilldown further.
- `levels`: Array of objects that provides layer info
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerOptions`: Same as layer options in `layers` option. If `url` is function, `url` takes argument as `properties` of parentLayer feature
- `legend`: configuration of legend to be added to layer. It requires [d3-legend](https://cdnjs.com/libraries/d3-legend). This creates a `<div class="map-legend">`.
- `position`: can be `topright`, `topleft`, `bottomleft` or `bottomright`(Defaults to `bottomright`)
- `format`: accepts d3 formats and applies to legend labels. (Defaults to `d`)
- `shape`: can be a d3 symbol or an svg path. Default `d3.symbolSquare`
- `size`: size of legend cell
- `cells`: number of cells in legend. Default `5`
- `width`: width of legend
- `height`: height of legend
- `scale`: accepts d3 scale format (mandatory). For examples, refer [d3-legend](https://d3-legend.susielu.com/#color-examples)
- `orient`: orientation of legend. Can be `vertical` (Default) or `horizontal`
- `shapeWidth`: width of legend cell. Default `20`
- `shapePadding`: padding of legend cell. Default `20`
- `labelOffset`: value to determine distance of label from each legend cell. Default `20`
## g1.mapviewer methods
`fitToLayer(layerName, options)`
Zooms map view to fit the layer. Supports same options as [fitBounds options](http://leafletjs.com/reference-1.3.0.html#fitbounds-options)
`zoomHandler(layerName, minZoomLevel, maxZoomLevel(optional) )`
Shows the layer with `layerName` only between `minZoomLevel` and `maxZoomLevel`.
`addNote(options)`
Adds a html label on the map. `options` is an object with following properties
- `html`: html object node
- `style`: Defaults to `bottom-left`. Specify styles for the `html`
- `position`: (Optional) Specifies the position of `html` on the map
`removeLayer(layerName)`
Removes the layer from the map and returns the layer if the layer exists on the map, else throws an error.
Note: This function will remove the layer from the map only. The layer object still exists in memory. Use `addLayer(layerName)` to add the layer back to the map.
`addLayer(layerName, layerConfig)`
- `addLayer(layerName)` if `layerName` layer does not exist on map, adds the layer to the map.
- `addLayer(layerName, layerConfig)` will creates a new layer with `layerConfig` options and adds it to the map.
## g1.mapviewer events
- `layersloaded` is fired when all layers are saved in mapviewer.gLayers (used interally).
- tooltip is rendered on each layer only after `layersload` is fired
- `layerName + 'loaded'` is fired for each layer with name as `layerName`
<!------------------------------------------------------------------------------------------->
## g1.mapviewer features examples
This creates a set of markers for each row in [cities.json](test/cities.json).
```html
......@@ -41,10 +195,6 @@ This creates a set of markers for each row in [cities.json](test/cities.json).
url: 'cities.json',
latitude: 'lat',
longitude: 'long',
options: {
// title: 'column-name' // TODO
}
// TODO: allow specifying styles (e.g. color, icon file, etc) from data
}
}
})
......@@ -78,38 +228,45 @@ You can apply styles based on any attribute or function.
</script>
```
This loads a [GeoJSON file](test/india-states.geojson), links data from
[state_score.json](test/state_score.json), and sets the fill color from a merged
attribute.
This adds legend to the map
```html
<div id="geojson-map" style="height:300px">
<div id="choropleth" class="map"></div>
<script>
var map = g1.mapviewer({
id: 'geojson-map',
var choro_map = g1.mapviewer({
id: 'choropleth',
legend: {
position: 'topright',
format: 'd',
shape: d3.symbolCircle,
size: 100,
scale: d3.scaleLinear().domain([10, 20, 30]).range(['red', 'yellow', 'green']),
orient: 'horizontal',
width: 300,
height: 100
},
layers: {
worldMap: { type: 'tile', url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
worldMap2: { type: 'tile', url: 'http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png' },
indiaGeojson: {
type: 'geojson',
url: 'india-states.geojson',
link: {
url: 'state_score.json', // Load data from this file
dataKey: 'name', // Join this column from the URL (data)
mapKey: 'ST_NM' // with this property in the GeoJSON
url: 'state_score.json',
dataKey: 'name',
mapKey: 'ST_NM'
},
options: {
style: {
fillColor: '#a00',
fillColor: '#ccc',
fillOpacity: 1
}
},
attrs: {
fillColor: { // Fill the regions
metric: 'score', // with the "score" column from state_score.json
scheme: 'RdYlGn' // using a RdYlGn gradient
},
tooltip: function(prop) { // On hover, show this HTML tooltip
return prop.ST_NM + ': ' + prop.TOT_P
fillColor: {
metric: 'score',
scale: 'linear',
domain: [10, 20, 30],
range: ['red', 'yellow', 'green'],
}
}
}
......@@ -135,13 +292,13 @@ Drilldown feature example:
dataKey: 'name', // Join this column from the URL (data)
mapKey: 'ST_NM' // with this property in the GeoJSON
},
tooltip: function(prop) { // On hover, show this HTML tooltip
return prop.ST_NM + ': ' + prop.TOT_P
},
attrs: {
fillColor: { // Fill the regions
metric: 'score', // with the "score" column state_score.json
range: 'RdYlGn' // using a RdYlGn gradient
},
tooltip: function(prop) { // On hover, show this HTML tooltip
return prop.ST_NM + ': ' + prop.TOT_P
}
}
}
......@@ -158,10 +315,10 @@ Drilldown feature example:
fillColor: {
metric: 'DT_CEN_CD',
range: 'RdYlGn'
},
tooltip: function (properties) {
return 'DISTRICT: ' + properties['DISTRICT']
}
},
tooltip: function (properties) {
return 'DISTRICT: ' + properties['DISTRICT']
}
}
}
......@@ -171,73 +328,38 @@ Drilldown feature example:
</script>
```
**Note**: You can use `type: 'topojson'` when loading TopoJSON maps.
Examples showing usage of mismatch label:
## g1.mapviewer options
- `id`: ID of the map DOM element (example: `mapid`), or the DOM element
- `map`:
- `options`: supports same options as [Map options](http://leafletjs.com/reference-1.3.0.html#map)
- `layers`: builds layers one on top of another in the specified order.
- `{layername: {...} }` dict with layer name as keys
- Each layer MUST have a type. Currently supported types are
- tile
- geojson
- topojson
- marker (`link`: option is not yet supported )
- circleMarker
- `tile` layer MUST have a url: that has the URL template for the leaflet tile layer.
- `url`: A string of the form - `http://{s}.somedomain.com/blabla/{z}/{x}/{y}{r}.png`
- `options`: supports same options as [Tile options](http://leafletjs.com/reference-1.3.0.html#tilelayer-minzoom)
- `geojson` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `options`: supports same options as [GeoJSON options](http://leafletjs.com/reference-1.3.0.html#geojson-style)
- `link`: adds attributes from input dataset to geojson
- `url` is url (String) to fetch data
- `mapKey` is attribute name in geojson to match
- `dataKey` is column name in input dataset that matches with geojson `mapKey`
- `attrs` Data driven styles. same as `options`. (`attrs` take priority over `options`)
- For `color`, `weight`, `opacity`, `fillColor`, `fillOpacity` properties, the options are:.
- `metric` string / function
- If `metric`: is a string, can be any numeric property of geojson
- To have a metric that is formala based on multiple properties, use function. Example: `function(row) { return row['congress_votes'] - row['bjp_votes']}`
- `domain` An array of two numbers. Defaults to calculated values of given `metric`.
- `range`
- For `fillColor` and `color`, must be a [interpolate color scheme](https://github.com/d3/d3-scale-chromatic#diverging)
- For `weight`, `opacity`,`fillOpacity` must be an array with min and max values
- `tooltip`: string / function that returns formatted value.
- function(properties) must return a string. feature properties are passed as argument.
- TODO: the properties currently include only geoJSON properties. link properties must be added
- `topojson` - same as `Geojson`
- `marker` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `latitude`: String (mandatory). Must be column name that contains latitude of marker
- `longitude`: String (mandatory). Must be column name that contains longitude of marker
- `options`: supports same options as [marker options](http://leafletjs.com/reference-1.3.0.html#marker-icon)
- `circleMarker` layer MUST have a data as an array of objects or else MUST have a url (string).
- `url`: String
- `data`: An array of objects. data: takes preference over url.
- `latitude`: String (mandatory). Must be column name that contains latitude of marker
- `longitude`: String (mandatory). Must be column name that contains longitude of marker
- `options`: supports same options as [circleMarker options](http://leafletjs.com/reference-1.3.0.html#circlemarker-radius)
- `attrs` same as `attrs` for `geojson` type layer
- `drilldown`:
- `rootLayer`: `geojson/topojson` layer that acts as root layer to drilldown further.
- `levels`: Array of objects that provides layer info
- `layerName`: Can be a string or function. Function takes argument as `properties` of parentLayer feature
- `layerOptions`: Same as layer options in `layers` option. If `url` is function, `url` takes argument as `properties` of parentLayer feature
## g1.mapviewer methods
`fitToLayer(layerName, options)`
Zooms map view to fit the layer. Supports same options as [fitBounds options](http://leafletjs.com/reference-1.3.0.html#fitbounds-options)
`zoomHandler(layerName, minZoomLevel, maxZoomLevel(optional) )`
Shows the layer with `layerName` only between `minZoomLevel` and `maxZoomLevel`.
```html
<div class="error-pange"></div>
<div id="mismatch_map" class="map"></div>
<script>
var custom_mismatch_map = g1.mapviewer({
id: 'custom-mismatch-map',
layers: {
indiaGeojson: {
type: 'geojson',
url: 'india-states.geojson',
link: {
url: state_scores,
dataKey: 'name',
mapKey: 'ST_NM',
mismatch: function (mismatch_array ) {
// Render custom message
var custom_message = `<h2>List of Data Merge Mismatches</h2>`
custom_message += `<table>`
mismatch_array.forEach(function(obj){
custom_message += `<tr><td>${obj.feature.properties.ST_NM}</td></tr>`
})
custom_message += `</table>`
$('.error-pane')
.html(custom_message)
}
}
}
}
})
## g1.mapviewer events
</script>
- `layersloaded` is fired when all layers are saved in mapviewer.gLayers (used interally).
- tooltip is rendered on each layer only after `layersload` is fired
- `layerName + 'loaded'` is fired for each layer with name as `layerName`
```
......@@ -61,7 +61,6 @@ export var MapViewer = class MapViewer {
for (let layerName in self.options.layers) {
self.buildLayer(layerName, self.options.layers[layerName])
}
self.initTooltipPopup()
self.drilldown()
self.current_level = 0
self.drilldown_stack = []
......@@ -147,7 +146,7 @@ MapViewer.prototype.cacheData = function (layerName, url) {
return self.gData[url]
}()
case 'object':
this.gData[JSON.stringify(url).hashCode()] = url
this.gData[hashCode(JSON.stringify(url))] = url
// TODO: Reload all layers that use this data
// This part will make mapviewer data reactive
return async function () {
......@@ -162,47 +161,95 @@ MapViewer.prototype.cacheData = function (layerName, url) {
* Private method
*/
MapViewer.prototype._saveLayer = function (layerName, layer) {
var self = this, allLayersLoaded = true
var self = this
self.gLayers[layerName] = layer
self.gLayers[layerName].addTo(self.map)
if ('layers' in self.options) {
for (var key in self.gLayers) {
if (!self.gLayers[key]) {
allLayersLoaded = false
}
}
var allLayersLoaded = Object.keys(self.gLayers).filter(function(layerName) { return !self.gLayers[layerName]}).length == 0
if (allLayersLoaded) self.fire('layersloaded')
self.renderTooltip(layerName, self.options.layers[layerName])
self.renderPopup(layerName, self.options.layers[layerName])
}
if (allLayersLoaded === true) {
self.fire('layersloaded')
}
MapViewer.prototype.addMismatchLabel = function(mismatch_count) {
var config = {
style: {
color: "black",
padding: "5px",
border: "2px solid #cccccc",
'border-radius': "10px",
'background-color': "#ffffff",
},
position: 'bottomleft'
}
var div = document.createElement('div')
div.setAttribute('class', 'mismatch-log')
div.innerHTML = mismatch_count + " Mismatches found!"
config.html = div
var self = this
self.addNote(config)
}
MapViewer.prototype.addNote = function(options) {
/*
Adds a html label on the map.
where options must include
- html: html object
- style: styles to add to that html <optional>
- position: specifies the position on the map. <optional>
*/
var self = this
var div_note = L.control({position: options.position || "bottomleft"})
div_note.onAdd = function (map) {
if(options.style) add_styles(options.html, options.style)
return options.html
}
div_note.addTo(self.map)
}
MapViewer.prototype.mergeData = function (mapJSON, dataTable, mapKey, dataKey) {
var dataTableIndex = {}
MapViewer.prototype.mergeData = function (mapJSON, dataTable, link) {
var dataTableIndex = {}, self = this
dataTable.forEach(function (row) {
var prop = typeof (row[dataKey]) == 'string' ? row[dataKey].toLowerCase() : row[dataKey]
var prop = typeof (row[link.dataKey]) == 'string' ? row[link.dataKey].toLowerCase() : row[link.dataKey]
dataTableIndex[prop] = row
})
function _merge_features(feature) {
var prop = typeof (feature.properties[mapKey]) == 'string' ? feature.properties[mapKey].toLowerCase() : feature.properties[mapKey]
var prop = typeof (feature.properties[link.mapKey]) == 'string' ? feature.properties[link.mapKey].toLowerCase() : feature.properties[link.mapKey]
var row = dataTableIndex[prop]
var status = {
isMatched: true,
feature: feature
}
for (let key in row)
feature.properties[key] = row[key]
if (row === undefined)
status.isMatched = false
return status
}
var mergeStatus = '', mismatchedFeatures = ''
switch (mapJSON.type) {
case 'Feature':
case 'FeatureCollection':
mapJSON.features.map(_merge_features)
mergeStatus = mapJSON.features.map(_merge_features)
mismatchedFeatures = mergeStatus.filter(e => !e.isMatched)
if(mismatchedFeatures && mismatchedFeatures.length > 0)
if (link.mismatch && (typeof link.mismatch === 'function')) link.mismatch(mismatchedFeatures)
else if(link && !link.mismatch) self.addMismatchLabel(mismatchedFeatures.length)
return mapJSON
case 'Topology':
mapJSON.objects[Object.keys(mapJSON.objects)[0]].geometries.map(_merge_features)
mergeStatus = mapJSON.objects[Object.keys(mapJSON.objects)[0]].geometries.map(_merge_features)
mismatchedFeatures = mergeStatus.filter(e => !e.isMatched)
if(mismatchedFeatures && mismatchedFeatures.length > 0)
if (link.mismatch && (typeof link.mismatch === 'function')) link.mismatch(mismatchedFeatures)
else if(link && !link.mismatch) self.addMismatchLabel(mismatchedFeatures.length)
return mapJSON
default:
mapJSON.map(function (json) {
var row = dataTableIndex[json[mapKey]]
var row = dataTableIndex[json[link.mapKey]]
for (let key in row) {
json[key] = row[key]
}
......@@ -225,6 +272,9 @@ MapViewer.prototype.off = function (eventName, callback, options) {
MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
var self = this, gLayer
if (!(layerName in self.options))
self.options.layers[layerName] = layerConfig
// Remove layer on map, if exists
if (self.map.hasLayer(self.gLayers[layerName])) self.map.removeLayer(self.gLayers[layerName])
......@@ -239,9 +289,9 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
self.cacheData(layerName, layerConfig[dataOrURL(layerConfig)]).then(function (mapJSON) {
if ('link' in layerConfig) {
self.cacheData(layerName, layerConfig.link[dataOrURL(layerConfig.link)]).then(function (tableData) {
self.mergeData(mapJSON, tableData, layerConfig.link.mapKey, layerConfig.link.dataKey)
self.mismatch_array = []
self.mergeData(mapJSON, tableData, layerConfig.link)
gLayer = new L.TopoJSON(mapJSON, layerConfig.options)
self._saveLayer(layerName, gLayer)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fitToLayer(layerName)
......@@ -249,7 +299,6 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
})
} else {
gLayer = new L.TopoJSON(mapJSON, layerConfig.options)
self._saveLayer(layerName, gLayer)
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
self.fitToLayer(layerName)
......@@ -288,7 +337,7 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
}
if ('link' in layerConfig) {
self.cacheData(layerName, layerConfig.link[dataOrURL(layerConfig.link)]).then(function (tableData) {
self.mergeData(pointjson, tableData, layerConfig.link.mapKey, layerConfig.link.dataKey)
self.mergeData(pointjson, tableData, layerConfig.link)
create_layer()
if ('attrs' in layerConfig) self._choropleth(layerName, layerConfig)
......@@ -306,6 +355,29 @@ MapViewer.prototype.buildLayer = function (layerName, layerConfig) {
}
}
MapViewer.prototype.removeLayer = function (layerName){
var self = this
if(self.map.hasLayer(self.gLayers[layerName]) && self.gLayers[layerName]){
let layer = self.gLayers[layerName]
self.map.removeLayer(layer)
return layer
}
else
throw new Error('Layer ' + layerName + ' is not available')
}
MapViewer.prototype.addLayer = function (layerName, layerConfig){
var self = this
if(layerName && layerConfig)
self.buildLayer(layerName, layerConfig)
else{
if (self.gLayers[layerName] && !self.map.hasLayer(self.gLayers[layerName]))
self.map.addLayer(self.gLayers[layerName])
else
throw new Error('Layer ' + layerName + ' is already existed or not created')
}
}
MapViewer.prototype._choropleth = function (layerName, layerConfig, filter) {
var layer = this.gLayers[layerName], self = this
......@@ -377,16 +449,19 @@ MapViewer.prototype._calculateMinMax = function (layer, metricFormula) {
})
return [minVal, maxVal]
}
MapViewer.prototype.drilldown = function (drilldown) {
var self = this
self.on('layersloaded', function() {
if (self.options.drilldown) {
self.drilldown_stack.push(self.options.drilldown.rootLayer)
self.drilldown_recursive(self.options.drilldown.rootLayer)
}
}, {
once: true
})
}
MapViewer.prototype.drilldown_recursive = function (currentLayer) {
var self = this
const levels = deepclone(self.options.drilldown.levels)
......@@ -399,36 +474,20 @@ MapViewer.prototype.drilldown_recursive = function (currentLayer) {
} else {
var nextLayer = deepclone(levels[self.current_level])
if (typeof (nextLayer.layerName) == 'function') {
nextLayer.layerName = nextLayer.layerName(sublayer.feature.properties)
}
if (typeof (nextLayer.layerOptions.url) == 'function') {
nextLayer.layerOptions.url = nextLayer.layerOptions.url(sublayer.feature.properties)
}
if (typeof (nextLayer.layerOptions.data) == 'function') {
nextLayer.layerOptions.data = nextLayer.layerOptions.data(sublayer.feature.properties)
}
if (nextLayer.layerOptions.link && typeof (nextLayer.layerOptions.link.data) == 'function') {
nextLayer.layerOptions.link.data = nextLayer.layerOptions.link.data(sublayer.feature.properties)
}
if (nextLayer.layerOptions.link && typeof (nextLayer.layerOptions.link.url) == 'function') {
nextLayer.layerOptions.link.url = nextLayer.layerOptions.link.url(sublayer.feature.properties)
}
function_resolver(sublayer, nextLayer, ['layerName'])
function_resolver(sublayer, nextLayer.layerOptions, ['url', 'data'])
function_resolver(sublayer, nextLayer.layerOptions.link, ['url', 'data'])
self.options.layers[nextLayer.layerName] = nextLayer.layerOptions
self.buildLayer(nextLayer.layerName, nextLayer.layerOptions)