Skip to content

Commit

Permalink
Refactor: Tile, Kernel, Renderer
Browse files Browse the repository at this point in the history
  • Loading branch information
fogleman committed Apr 8, 2016
1 parent cf11290 commit 5e9efb0
Show file tree
Hide file tree
Showing 7 changed files with 186 additions and 152 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ Once the data is loaded into Cassandra, the tile server can be run for rendering
| -table | points | Cassandra table to load from |
| -zoom | 18 | Zoom level that was used for binning points |
| -port | 5000 | Tile server port number |
| -cache | cache | Directory for caching tile images |

### Serving Maps

Expand Down
23 changes: 23 additions & 0 deletions kernel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package density

import "math"

type KernelItem struct {
Dx, Dy int
Weight float64
}

type Kernel []KernelItem

func NewKernel(n int) Kernel {
var result Kernel
for dy := -n; dy <= n; dy++ {
for dx := -n; dx <= n; dx++ {
d := math.Sqrt(float64(dx*dx + dy*dy))
w := math.Max(0, 1-d/float64(n))
w = math.Pow(w, 2)
result = append(result, KernelItem{dx, dy, w})
}
}
return result
}
3 changes: 2 additions & 1 deletion loader/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ create table points (
);
*/

const CqlHost = "127.0.0.1"
const Workers = 64

var Keyspace string
Expand Down Expand Up @@ -70,7 +71,7 @@ func main() {
Query = "INSERT INTO %s (zoom, x, y, lat, lng) VALUES (?, ?, ?, ?, ?);"
Query = fmt.Sprintf(Query, Table)

cluster := gocql.NewCluster("127.0.0.1")
cluster := gocql.NewCluster(CqlHost)
cluster.Keyspace = Keyspace
session, _ := cluster.CreateSession()
defer session.Close()
Expand Down
71 changes: 71 additions & 0 deletions renderer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package density

import (
"fmt"
"image"
"log"
"math"

"github.com/gocql/gocql"
)

type Renderer struct {
Cluster *gocql.ClusterConfig
Query string
BaseZoom int
}

func NewRenderer(host, keyspace, table string, baseZoom int) *Renderer {
cluster := gocql.NewCluster(host)
cluster.Keyspace = keyspace
query := "SELECT lat, lng FROM %s WHERE zoom = ? AND x = ? AND y = ?;"
query = fmt.Sprintf(query, table)
return &Renderer{cluster, query, baseZoom}
}

func (r *Renderer) Render(zoom, x, y int) (image.Image, bool) {
session, _ := r.Cluster.CreateSession()
defer session.Close()
tile := r.loadTile(session, zoom, x, y)
kernel := NewKernel(2)
scale := 32 / math.Pow(4, float64(r.BaseZoom-zoom))
return tile.Render(kernel, scale)
}

func (r *Renderer) loadPoints(session *gocql.Session, x, y int, tile *Tile) {
iter := session.Query(r.Query, r.BaseZoom, x, y).Iter()
var lat, lng float64
for iter.Scan(&lat, &lng) {
tile.Add(lat, lng)
}
if err := iter.Close(); err != nil {
log.Fatal(err)
}
}

func (r *Renderer) loadTile(session *gocql.Session, zoom, x, y int) *Tile {
tile := NewTile(zoom, x, y)
if zoom < 12 {
return tile
}
p := 1 // padding
var x0, y0, x1, y1 int
if zoom > r.BaseZoom {
d := int(math.Pow(2, float64(zoom-r.BaseZoom)))
x0, y0 = x/d-p, y/d-p
x1, y1 = x/d+p, y/d+p
} else if zoom < r.BaseZoom {
d := int(math.Pow(2, float64(r.BaseZoom-zoom)))
x0, y0 = x*d-p, y*d-p
x1, y1 = (x+1)*d-1+p, (y+1)*d-1+p
} else {
x0, y0 = x-p, y-p
x1, y1 = x+p, y+p
}
for tx := x0; tx <= x1; tx++ {
for ty := y0; ty <= y1; ty++ {
r.loadPoints(session, tx, ty, tile)
}
}
return tile
}
147 changes: 12 additions & 135 deletions server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,173 +3,57 @@ package main
import (
"flag"
"fmt"
"image"
"image/color"
"image/png"
"log"
"math"
"net/http"
"os"
"path"
"strconv"

"github.com/fogleman/density"
"github.com/gocql/gocql"
"github.com/gorilla/mux"
"github.com/lucasb-eyer/go-colorful"
)

const CachePath = "cache"
const CqlHost = "127.0.0.1"

var Port int
var CacheDirectory string
var Keyspace string
var Table string
var Zoom int

var Cluster *gocql.ClusterConfig
var Query string
var BaseZoom int

func init() {
flag.IntVar(&Port, "port", 5000, "server port")
flag.StringVar(&CacheDirectory, "cache", "cache", "cache directory")
flag.StringVar(&Keyspace, "keyspace", "density", "keyspace name")
flag.StringVar(&Table, "table", "points", "table name")
flag.IntVar(&Zoom, "zoom", 18, "tile zoom")
flag.IntVar(&BaseZoom, "zoom", 18, "tile zoom")
}

type kernelItem struct {
dx, dy int
w float64
func cachePath(zoom, x, y int) string {
return fmt.Sprintf("%s/%d/%d/%d.png", CacheDirectory, zoom, x, y)
}

var kernel []kernelItem

func init() {
n := 2
for dy := -n; dy <= n; dy++ {
for dx := -n; dx <= n; dx++ {
d := math.Sqrt(float64(dx*dx + dy*dy))
w := math.Max(0, 1-d/float64(n))
w = math.Pow(w, 2)
kernel = append(kernel, kernelItem{dx, dy, w})
}
}
func pathExists(p string) bool {
_, err := os.Stat(p)
return err == nil
}

func parseInt(x string) int {
value, _ := strconv.ParseInt(x, 0, 0)
return int(value)
}

type Point struct {
X, Y float64
}

type Key struct {
X, Y int
}

func loadPoints(session *gocql.Session, zoom, x, y, tx, ty int, grid map[Key]float64) int {
lat2, lng1 := density.TileLatLng(zoom, x, y)
lat1, lng2 := density.TileLatLng(zoom, x+1, y+1)

iter := session.Query(Query, Zoom, tx, ty).Iter()
var rows int
var lat, lng float64
for iter.Scan(&lat, &lng) {
kx := int(math.Floor((lng - lng1) / (lng2 - lng1) * 256))
ky := int(math.Floor((lat - lat1) / (lat2 - lat1) * 256))
grid[Key{kx, ky}]++
rows++
}

if err := iter.Close(); err != nil {
log.Fatal(err)
}

return rows
}

func getPoints(session *gocql.Session, zoom, x, y int, grid map[Key]float64) int {
if zoom < 12 {
return 0
}
p := 1 // padding
var x0, y0, x1, y1 int
if zoom > Zoom {
d := int(math.Pow(2, float64(zoom-Zoom)))
x0, y0 = x/d-p, y/d-p
x1, y1 = x/d+p, y/d+p
} else if zoom < Zoom {
d := int(math.Pow(2, float64(Zoom-zoom)))
x0, y0 = x*d-p, y*d-p
x1, y1 = (x+1)*d-1+p, (y+1)*d-1+p
} else {
x0, y0 = x-p, y-p
x1, y1 = x+p, y+p
}
var rows int
for tx := x0; tx <= x1; tx++ {
for ty := y0; ty <= y1; ty++ {
rows += loadPoints(session, zoom, x, y, tx, ty, grid)
}
}
return rows
}

func render(grid map[Key]float64, scale float64) (image.Image, bool) {
im := image.NewNRGBA(image.Rect(0, 0, 256, 256))
ok := false
for y := 0; y < 256; y++ {
for x := 0; x < 256; x++ {
var t, tw float64
for _, k := range kernel {
nx := x + k.dx
ny := y + k.dy
t += grid[Key{nx, ny}] * k.w
tw += k.w
}
if t == 0 {
continue
}
t *= 32
t /= scale
t /= tw
t = t / (t + 1)
a := uint8(255 * math.Pow(t, 0.5))
c := colorful.Hsv(215.0, 1-t*t, 1)
r, g, b := c.RGB255()
im.SetNRGBA(x, 255-y, color.NRGBA{r, g, b, a})
ok = true
}
}
return im, ok
}

func cachePath(zoom, x, y int) string {
return fmt.Sprintf("%s/%d/%d/%d.png", CachePath, zoom, x, y)
}

func pathExists(p string) bool {
_, err := os.Stat(p)
return err == nil
}

func Handler(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
zoom := parseInt(vars["zoom"])
x := parseInt(vars["x"])
y := parseInt(vars["y"])

p := cachePath(zoom, x, y)
if !pathExists(p) {
// nothing in cache, render the tile
session, _ := Cluster.CreateSession()
defer session.Close()
grid := make(map[Key]float64)
scale := math.Pow(4, float64(Zoom-zoom))
rows := getPoints(session, zoom, x, y, grid)
fmt.Println(zoom, x, y, rows)
im, ok := render(grid, scale)
renderer := density.NewRenderer(CqlHost, Keyspace, Table, BaseZoom)
im, ok := renderer.Render(zoom, x, y)
if ok {
// save tile in cache
d, _ := path.Split(p)
Expand All @@ -196,13 +80,6 @@ func Handler(w http.ResponseWriter, r *http.Request) {

func main() {
flag.Parse()

Query = "SELECT lat, lng FROM %s WHERE zoom = ? AND x = ? AND y = ?;"
Query = fmt.Sprintf(Query, Table)

Cluster = gocql.NewCluster("127.0.0.1")
Cluster.Keyspace = Keyspace

router := mux.NewRouter()
router.HandleFunc("/{zoom:\\d+}/{x:\\d+}/{y:\\d+}.png", Handler)
addr := fmt.Sprintf(":%d", Port)
Expand Down
77 changes: 77 additions & 0 deletions tile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package density

import (
"image"
"image/color"
"math"

"github.com/lucasb-eyer/go-colorful"
)

const TileSize = 256

func TileXY(zoom int, lat, lng float64) (x int, y int) {
x = int(math.Floor((lng + 180.0) / 360.0 * (math.Exp2(float64(zoom)))))
y = int(math.Floor((1.0 - math.Log(math.Tan(lat*math.Pi/180.0)+1.0/math.Cos(lat*math.Pi/180.0))/math.Pi) / 2.0 * (math.Exp2(float64(zoom)))))
return
}

func TileLatLng(zoom, x, y int) (lat, lng float64) {
n := math.Pi - 2.0*math.Pi*float64(y)/math.Exp2(float64(zoom))
lat = 180.0 / math.Pi * math.Atan(0.5*(math.Exp(n)-math.Exp(-n)))
lng = float64(x)/math.Exp2(float64(zoom))*360.0 - 180.0
return
}

type IntPoint struct {
X, Y int
}

type Tile struct {
Zoom, X, Y int
Lat0, Lng0 float64
Lat1, Lng1 float64
Grid map[IntPoint]float64
}

func NewTile(zoom, x, y int) *Tile {
lat1, lng0 := TileLatLng(zoom, x, y)
lat0, lng1 := TileLatLng(zoom, x+1, y+1)
grid := make(map[IntPoint]float64)
return &Tile{zoom, x, y, lat0, lng0, lat1, lng1, grid}
}

func (tile *Tile) Add(lat, lng float64) {
// TODO: bilinear interpolation
x := int(math.Floor((lng - tile.Lng0) / (tile.Lng1 - tile.Lng0) * TileSize))
y := int(math.Floor((lat - tile.Lat0) / (tile.Lat1 - tile.Lat0) * TileSize))
tile.Grid[IntPoint{x, y}]++
}

func (tile *Tile) Render(kernel Kernel, scale float64) (image.Image, bool) {
im := image.NewNRGBA(image.Rect(0, 0, TileSize, TileSize))
ok := false
for y := 0; y < TileSize; y++ {
for x := 0; x < TileSize; x++ {
var t, tw float64
for _, k := range kernel {
nx := x + k.Dx
ny := y + k.Dy
t += tile.Grid[IntPoint{nx, ny}] * k.Weight
tw += k.Weight
}
if t == 0 {
continue
}
t *= scale
t /= tw
t = t / (t + 1)
a := uint8(255 * math.Pow(t, 0.5))
c := colorful.Hsv(215.0, 1-t*t, 1)
r, g, b := c.RGB255()
im.SetNRGBA(x, TileSize-1-y, color.NRGBA{r, g, b, a})
ok = true
}
}
return im, ok
}
Loading

0 comments on commit 5e9efb0

Please sign in to comment.