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

Deck.gl Column Layer #613

Open
bskrt opened this issue Jul 16, 2019 · 6 comments
Open

Deck.gl Column Layer #613

bskrt opened this issue Jul 16, 2019 · 6 comments
Assignees

Comments

@bskrt
Copy link

bskrt commented Jul 16, 2019

Having access to the deck.gl column layer would nice.
In some cases you don't want to use point aggregation in a specific area but just visualize a column at a certain coordinate with the height based on a certain value.

Afaik this isn't available at the moment?

@heshan0131
Copy link
Contributor

you are right, column is only used in the aggregation layers. You can, however, implement a custom layer. The docs is not complete yet. But you can take a look at this PR #540

@Harrisandwich
Copy link
Contributor

I've been able to throw together a few custom layers if you have questions. Still trying to figure out some of the finer details until the docs are done, but I'm getting to a point where I feel like I know the steps.

@heshan0131
Copy link
Contributor

heshan0131 commented Jul 24, 2019

@Harrisandwich I would be interested to know your experience of implementing custom layer. I am not at a point where I am happy with the clarity and ease of use of current layer class methods. I am constantly making changes to it, that's why I hesitate to expose it at the moment

@Harrisandwich
Copy link
Contributor

(Bit of a long comment hahaha hopefully you find all this helpful)

It hasn't been too hard to figure out. The first custom layer was the hardest, as it was extending the MapboxLayer class you use for heatmaps. The next few have gone better so far.

I'd say my biggest roadblocks creating custom layers are:

  • Not really knowing the difference between visConfig properties and visualChannels. Still not totally clear on this
  • Getting layer config controls for my custom layer into the existing kepler sidebar (see below)
  • I still don't really know how to set the default column for a layer. I can set the required column, but I can't get the layer to automatically use a column like I see in other layers

Mapbox layer specific

  • Since the mapbox layer class is built for almost exclusively for heatmap usage, I got tripped up on the required columns being lat and lng, but I eventually figured out how to get around that by declaring my own.
  • I had to use a custom version of the updateMapboxLayers function in mapbox-utils so it would add a vector source, among other things.
  • Interaction with mapbox layer types, as I mention in Does Kepler allow tileserver layers to pop a tooltip? #623, was challenging, as the 'clicked' field in the store doesn't change when there is no kepler feature present. So if a click happens on a mapbox layer, there is no current way to get what was clicked.

Below are a few blocks used to make custom layers happen in our app

In order to get custom layer controls to show I had to create my own _render function and add it to the LayerConfigurator like so:

import LayerConfigurator, { LayerColorSelector }
  from 'kepler.gl/dist/components/side-panel/layer-panel/layer-configurator'

LayerConfigurator.prototype._renderTileLayerConfig = ({
  layer,
  visConfiguratorProps,
  layerConfiguratorProps,
}) => (
// ...component stuff
)

Also in order to get kepler to use the layer I added it to the layerClasses store like so:

import * as Layers from 'kepler.gl/layers'
import * as CustomLayers from '../common-components/custom-kepler-layers'


Object.values(CustomLayers).forEach((cl) => { Layers.LayerClasses[cl._typeName] = cl })

const customizedKeplerGlReducer = keplerGlReducer
  .initialState({
    visState: { layerClasses: Layers.LayerClasses },
    uiState: { currentModal: null },
  })

For your consideration, here is the "TileClass" I made for showing MVT layers:

/* eslint-disable no-unused-vars */
/* eslint-disable class-methods-use-this */
import Color from 'color'
import { ALL_FIELD_TYPES } from 'kepler.gl/constants'
import MapboxLayerGL from 'kepler.gl/dist/layers/mapboxgl-layer'
import GeojsonLayerIcon from 'kepler.gl/dist/layers/geojson-layer/geojson-layer-icon'
import { createSelector } from 'reselect'
import { getPolygonLayerFillCondition } from '../../helpers/mapbox-map-utils/dynamic-styling'
import { calculateRange } from '../../helpers/utils'


export const tileVisConfigs = {
  opacity: 'opacity',
  colorRange: 'colorRange',
  type: {
    type: 'string',
    defaultValue: 'demographic',
  },
}

class MapboxTileLayer extends MapboxLayerGL {
  constructor(props) {
    super(props)
    this.registerVisConfig(tileVisConfigs)
  }

  get type() {
    return 'tile'
  }

  get visualChannels() {
    return {}
  }

  get layerIcon() {
    return GeojsonLayerIcon
  }
  get requiredLayerColumns() {
    return ['category', 'style']
  }

  getVisualChannelDescription(channel) {
    return channel === 'color' ? {
      label: 'color',
      measure: 'Density',
    } : {}
  }

  datasetSelector = config => config.dataId
  columnsSelector = config => config.columns
  dataSelector = config => config.data
  isVisibleSelector = config => config.isVisible
  visConfigSelector = config => config.visConfig
  colorSelector = config => config.color

  computeTileConfiguration = createSelector(
    this.datasetSelector,
    this.columnsSelector,
    this.dataSelector,
    this.isVisibleSelector,
    this.visConfigSelector,
    this.colorSelector,
    (
      datasetId,
      columns,
      data,
      isVisible,
      visConfig,
      color,
    ) => {
// Alot of what is happening here is VERY specific to our app. I'll probably try to create a more generic tile layer type at some point.
      const { opacity } = visConfig
      const category = JSON.parse(data[columns.category.fieldIdx])
      const colorObject = Color(color)
      let style = JSON.parse(data[columns.style.fieldIdx])
      const range = calculateRange(style.length).reverse()
      style = style.map((s, i) => ({
        color:
          Color(`hsl(${colorObject.hsl().color[0]}, 70%, ${range[i]}%)`)
            .rgb()
            .alpha(Color(s.color.color).alpha()),
        data: s.data,
      }))
      const mapLayerID = category.table || category.slug
      const paint = {
        'fill-color': [
          'case',
          ...getPolygonLayerFillCondition(style, mapLayerID),
        ],
        'fill-opacity': [
          'case',
          // if there's no value, don't show feature (transparent)
          ['==', ['feature-state', mapLayerID], null],
          0,
          // else if hover, use 1 for opacity
          opacity,
        ],
      }
      const layer = {
        _sourceType: 'vector',
        _categoryIndex: columns.category.fieldIdx,
        type: 'fill',
        id: this.id,
        source: category.resolution,
        'source-layer': category.resolution,
        layout: { visibility: isVisible ? 'visible' : 'none' },
        paint,
      }

      return layer
    },
  )

  formatLayerData(_, allData, filteredIndex, oldLayerData, opt = {}) {
    const config = this.computeTileConfiguration({
      ...this.config,
      data: allData,
    })
    config.id = this.id
    return {
      columns: this.config.columns,
      config,
      data: allData,
    }
  }
}

MapboxTileLayer._typeName = 'tile' // this is for easily appending new custom layers to the layerClasses store (see above)

export default MapboxTileLayer

and my changes to the updateMapboLayers:

export default function updateMapboxLayers(
  map,
  newLayers = [],
  oldLayers = null,
  mapLayers = null,
  opt = { force: false }, 

/*
 IMPORTANT
 Having force set to true by default caused some problems, as it would remove layers even if 
 the config didn't change. Lot's of confusion in trying to figure out why my layers were disappearing
*/ 
) {
  if (newLayers.length > 0 && Object.keys(oldLayers).length === 0) {
// all that interaction stuff I mentioned in #623 
    document
      .querySelector('.maps')
      .addEventListener('mousemove', e => onMouseHoverEvent(e, map, 'mousemove'))
    document
      .querySelector('.maps')
      .addEventListener('mouseenter', e => onMouseHoverEvent(e, map, 'mouseenter'))
    document
      .querySelector('.maps')
      .addEventListener('mouseleave', e => onMouseHoverEvent(e, map, 'mouseleave'))
    document
      .querySelector('.maps')
      .addEventListener('mousedown', e => onMapMouseDown(e, map))
    document
      .querySelector('.maps')
      .addEventListener('mouseup', e => onMapMouseUp(e, map))
  }
  // delete non existing layers
  if (oldLayers) {
    const oldLayersKeys = Object.keys(oldLayers)
    if (newLayers.length === 0 && oldLayersKeys.length > 0) {
      oldLayersKeys.forEach(layerId => map.removeLayer(layerId))
    } else {
      // remove layers
      const currentLayersIds = newLayers.reduce((final, layer) => ({
        ...final,
        [layer.id]: true,
      }), {})

      const layersToDelete = oldLayersKeys.reduce((final, layerId) => {
        // if layer doesn't exists anymore
        if (!currentLayersIds[layerId]) {
          return {
            ...final,
            [layerId]: oldLayers[layerId],
          }
        }
        return final
      }, [])
      Object.keys(layersToDelete).forEach(layerId => map.removeLayer(layerId))
    }
  }

  // insert or update new layer
  // TODO: fix complexity
  /* eslint-disable complexity */
  newLayers.forEach((overlay) => {
    const { id: layerId, config, data, datasetId } = overlay
    if (!data && !config) {
      return
    }
    const isAvailableAndVisible =
      !(mapLayers && mapLayers[layerId]) || mapLayers[layerId].isVisible
    // checking if source already exists
    if (data && isAvailableAndVisible) {
      if (config.type === 'fill' && config._sourceType === 'vector') {
        const category = JSON.parse(data[config._categoryIndex])
        const source = map.getSource(category.resolution)
        if (!source) {
          map.addSource(category.resolution, {
            type: 'vector',
            tiles: [data[0]],
            minzoom: 6,
            maxzoom: 14,
          })
        }
      } else {
        const source = map.getSource(datasetId)
        if (!source) {
          map.addSource(datasetId, {
            type: 'geojson',
            data,
          })
        } else {
          source.setData(data)
        }
      }
    }
    const oldConfig = oldLayers[layerId]
    const mapboxLayer = map.getLayer(layerId)
    // compare with previous configs

    if (!oldConfig || oldConfig !== config || !mapboxLayer || opt.force) {
      // check if layer already is set
      // remove it if exists
      if (mapboxLayer) {
        map.removeLayer(layerId)
      }
      // add if visible and available
      if (isAvailableAndVisible) {
        map.addLayer(config)
      }
    }
  })
  /* eslint-enable complexity */
}

@punov
Copy link
Contributor

punov commented Jun 23, 2021

@Harrisandwich we're struggling with pretty much the same task in our project, trying to render MVTLayer providing the PBF url to it on top of our Kepler app. It was always easy and straightforward with DeckGL itself:
https://deck.gl/docs/api-reference/geo-layers/mvt-layer

So I was happy to see your PoC, but could you please provide a bit more details / code example of how exactly to use your MapboxTileLayer?
I suppose, you'd have to use addDataToMap here anyways to properly push data onto kepler, but how the MapboxTileLayer involved then?

We're close to do the same workaround, but it's still not working properly, so maybe after all these years you have some more to share :) Thanks in advance!

@Harrisandwich
Copy link
Contributor

Hello @punov. Unfortunately, I don't work at the company I was at when I made this comment, so I don't have access to the code. It was also quite a while ago, so I can't really remember what exactly I did to make this work. If I remember correctly, the MVTLayer is based off the Kepler heatmap layer (which uses mapbox) to render. You should be able to implement it in a similar way, though getting interaction to work might be tough. Also I mention in a comment in the code blocks above that some of the logic in the layer itself is very specific to what I was working on at the time, so it may need to be adjusted to work for you

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

4 participants