Making Voxel tiles from LAZ using pdal and Tippecanoe

# Introdution
I have developed voxel tile from a point cloud data (LAZ) format. This is a record of my work.


# Envrionment
* Windows 10 Enterprise
* Docker version 20.10.8


# Procedure

## Step 1: Starting Docker container
Because we need some software for the data conversion, let's use Docker. For window's user, it is sometimes hard to use the vector tile conversion tool, Tippecanoe, in their local environment, so it would be easier to use Docker container to do the conversion.

We have unvt/voxel image, so we can use it if needed.

```bash
docker pull unvt/voxel
docker run -it --rm -v ${PWD}:/data unvt/voxel
cd /data
```

## Step 2: Reprojection to the web mercator projection and export to LAS file
As the source data is in LAZ format and its projection is UTM17 (EPSG:32617), we will reproject it and save it as LAS file. We will use the pdal (PDAL - Point Data Abstraction Library) with some help from ruby (note: ruby is an open source programming language).

I prepared 1.rb and constant.rb for reprojection using pdal. The constants.rb includes some other constants for our future work. 

```ruby:1.rb
# for reprojection. 
require './constants'
require 'yaml'
require 'json'

basename = ENV['BASENAME']
src_path = "#{TMP_DIR}/#{basename}.laz"
dst_path = "#{TMP_DIR}/#{basename}-3857.las"

pipeline = <<-EOS
pipeline: 
  - 
    filename: #{src_path}
    type: readers.las
    spatialreference: "EPSG:32617"
  -
    type: filters.reprojection
    out_srs: "EPSG:3857"
  -
    type: writers.las
    filename: #{dst_path}
EOS

print JSON.dump(YAML.load(pipeline))
```

```ruby:consants.rb
SKIP = true

TMP_DIR = 'src'
Z_ONE_METER = 19
BASE = 2

MAXZOOM = 19
MINZOOM = 10
MINCOPYZOOM = 10

LAYER = 'voxel'
```

```bash
BASENAME=tappan ruby 1.rb | pdal pipeline --stdin
```


## Step 3: Resampling
I made 2.rb for resmpling the input data with pdal. Because I defined Z-ONE_METER as 19 in the constraints.rb, the spacing will be 1 meter at the zoom level 19. (The spacing will be larger in smaller zoom level. e.g 2 meter at ZL18, 4 meter at ZL 17, etc.)


```ruby: 2.rb
require './constants'
require 'yaml'
require 'json'

z = ENV['Z'].to_i
spacing = (BASE ** (Z_ONE_METER - z)).to_f
basename = ENV['BASENAME']
src_path = "#{TMP_DIR}/#{basename}-3857.las" #unless File.exist?(src_path)
dst_path = "#{TMP_DIR}/#{basename}-#{z}.las"
$stderr.print "#{spacing}m for #{dst_path} from #{src_path}\n"

pipeline = <<-EOS
pipeline: 
  - 
    filename: #{src_path}
    type: readers.las
  -
    type: filters.voxelcenternearestneighbor
    cell: #{spacing}
  -
    type: writers.text
    format: csv
    filename: STDOUT
EOS

print JSON.dump(YAML.load(pipeline))
```

Then, run the pdal as below. Because the output is not piped into a vector tile conversion tool, tippecanoe, we just see the result as the stdout. It seems that their RGB is also properly extracted.

```bash
Z=19 BASENAME=tappan ruby 2.rb | pdal pipeline --stdin
```
We can see that coordinates are in the web mercator, and spacing of each voxel is one meter. We can also see that the values for RGB appear at the end of the row. Please check their order so that we can use them at the following step (This case, they are 13th, 14th and 15th.).
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/a34f8e95-3e39-2bbf-2020-d3c2db3db5b2.png)

## Step 4: Adjustment for VT conversion
Because the vector tile conversion tool, Tippecanoe, reads GeoJSON as input, we will edit the previous result for that format. In addtion, we develop the data structure for each recortd to be converted as vector tile.

```ruby: 3.rb
require './constants'
require 'yaml'
require 'json'

z = ENV['Z'].to_i
minzoom = (z == MINZOOM) ? z  : MINCOPYZOOM
maxzoom = z
spacing = (BASE ** (Z_ONE_METER - z)).to_f
n = 0

first = true
start_time = Time.now
while gets
  if first
    first = false
    next
  else
    n += 1
  end
  r = $_.strip.split(',')
  x = r[0].to_f - r[0].to_f % spacing
  y = r[1].to_f - r[1].to_f % spacing
  h = r[2].to_f - r[2].to_f % spacing
  h = h.to_i
  color = '#' + r[13..15].map{|v| sprintf('%01x', v.to_i )}.join
  g = <<-EOS
type: Polygon
coordinates: 
  -
    -
      - #{x}
      - #{y}
    -
      - #{x + spacing}
      - #{y}
    -
      - #{x + spacing}
      - #{y + spacing}
    -
      - #{x}
      - #{y + spacing}
    -
      - #{x}
      - #{y}
  EOS
  g = YAML.load(g)
  f = <<-EOS
type: Feature
properties: 
  color: '#{color}'
  h: #{h}
  spacing: #{spacing}
tippecanoe:
  minzoom: #{minzoom}
  maxzoom: #{maxzoom}
  layer: #{LAYER}
  EOS
  f = YAML.load(f)
  f[:geometry] = g
  print JSON.dump(f), "\n"
end

def hms(s)
  min, sec = s.to_i.divmod(60)
  hour, min = min.divmod(60)
  "%02d:%02d:%02d" % [hour, min, sec]
end

$stderr.print "\n[#{z}] #{hms(Time.now - start_time)}s\n"
```

```bash
Z=19 BASENAME=tappan ruby 2.rb | pdal pipeline --stdin | Z=19 ruby 3.rb
```
Then, you can confirm the geojson output as stdout. This will be forwarded to Tippecanoe at the next stage.
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/cfbf3b7f-9533-96e1-253e-f8e671b1524d.png)

## Step 5. Vector tile conversion
Then, we forward the result from the previous step into Tippecanoe by using pipe funciton. You can do it by just running the following command.

```
Z=19 BASENAME=tappan ruby 2.rb | pdal pipeline --stdin | Z=19 ruby 3.rb | tippecanoe --maximum-zoom=19 --minimum-zoom=19 --projection=EPSG:3857 --force --output out-19.mbtiles --no-tile-size-limit --no-feature-limit
```
The, you will get the vector tile in mbtiles format.

## Step 6. From mbtiles to pbf format
We convert the mbtiles file into pbf files so that we can host them via GitHub page.

```
tile-join --force --output-to-directory=docs/voxel --no-tile-size-limit --no-tile-compression output.mbtiles
```

## Step 7. Hosting voxel tiles
Using MapLibre GL JS, we can create a web map like this.

https://ubukawa.github.io/tappanSQ/index3.html#15.56/41.293028/-82.216623/85.2/44
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/679143/8dd593a0-f69c-dd66-6b27-6c31b9d47f70.png)

I do not explain how we can create such webmap, but you can check necessary files for the hosting from here: https://github.com/ubukawa/tappanSQ/tree/main/docs

# Acknowledgement
I thank Steve V. M. for sharing the test data.

# References
https://pdal.io/en/stable/

https://github.com/ubukawa/tappanSQ