diff --git a/expire/tilelist.go b/expire/tilelist.go index ec2345b..b84fb06 100644 --- a/expire/tilelist.go +++ b/expire/tilelist.go @@ -3,6 +3,7 @@ package expire import ( "fmt" "io" + "math" "os" "path/filepath" "sync" @@ -30,31 +31,21 @@ func init() { } } -// fraction of a tile that is added as a padding around an expired tile -const tilePadding = 0.1 - -func TileCoords(long, lat float64, zoom uint32) []tileKey { +func tileCoord(long, lat float64, zoom int) (float64, float64) { x, y := proj.WgsToMerc(long, lat) res := mercRes[zoom] x = x - mercBbox[0] y = mercBbox[3] - y - tileX := float32(x / (res * 256)) - tileY := float32(y / (res * 256)) - - tiles := make([]tileKey, 0, 4) - for x := uint32(tileX - tilePadding); x <= uint32(tileX+tilePadding); x++ { - for y := uint32(tileY - tilePadding); y <= uint32(tileY+tilePadding); y++ { - tiles = append(tiles, tileKey{x, y}) - } - } - return tiles + tileX := float64(x / (res * 256)) + tileY := float64(y / (res * 256)) + return tileX, tileY } type TileList struct { mu sync.Mutex tiles map[tileKey]struct{} - zoom uint32 + zoom int out string } @@ -72,27 +63,80 @@ type tile struct { func NewTileList(zoom int, out string) *TileList { return &TileList{ tiles: make(map[tileKey]struct{}), - zoom: uint32(zoom), + zoom: zoom, mu: sync.Mutex{}, out: out, } } -func (tl *TileList) addCoord(long, lat float64) { - tl.mu.Lock() - for _, t := range TileCoords(long, lat, tl.zoom) { - tl.tiles[t] = struct{}{} - } - tl.mu.Unlock() -} - func (tl *TileList) Expire(long, lat float64) { tl.addCoord(long, lat) } func (tl *TileList) ExpireNodes(nodes []element.Node, closed bool) { - for _, nd := range nodes { - tl.addCoord(nd.Long, nd.Lat) + if len(nodes) == 0 { + return + } + if closed { + box := nodesBbox(nodes) + tiles := numBboxTiles(box, tl.zoom) + if tiles > 500 { + tl.expireLine(nodes) + } else { + tl.expireBox(box) + } + } else { + tl.expireLine(nodes) + } +} + +// expire a single point. Point is padded by 0.2 tiles to expire nearby tiles +// for nodes at a tile border. +func (tl *TileList) addCoord(long, lat float64) { + // fraction of a tile that is added as a padding around a single node + const tilePadding = 0.2 + tl.mu.Lock() + tileX, tileY := tileCoord(long, lat, tl.zoom) + for x := uint32(tileX - tilePadding); x <= uint32(tileX+tilePadding); x++ { + for y := uint32(tileY - tilePadding); y <= uint32(tileY+tilePadding); y++ { + tl.tiles[tileKey{x, y}] = struct{}{} + } + } + tl.mu.Unlock() +} + +// expireLine expires all tiles that are intersected by the line segments +// between the nodes +func (tl *TileList) expireLine(nodes []element.Node) { + if len(nodes) == 1 { + tl.addCoord(nodes[0].Long, nodes[0].Lat) + return + } + tl.mu.Lock() + defer tl.mu.Unlock() + for i := 0; i < len(nodes)-1; i++ { + x1, y1 := tileCoord(nodes[i].Long, nodes[i].Lat, tl.zoom) + x2, y2 := tileCoord(nodes[i+1].Long, nodes[i+1].Lat, tl.zoom) + if int(x1) == int(x2) && int(y1) == int(y2) { + tl.tiles[tileKey{X: uint32(x1), Y: uint32(y1)}] = struct{}{} + } else { + for _, tk := range bresenham(x1, y1, x2, y2) { + tl.tiles[tk] = struct{}{} + } + } + } +} + +// expireBox expires all tiles inside the bbox +func (tl *TileList) expireBox(b bbox) { + tl.mu.Lock() + defer tl.mu.Unlock() + x1, y1 := tileCoord(b.minx, b.maxy, tl.zoom) + x2, y2 := tileCoord(b.maxx, b.miny, tl.zoom) + for x := uint32(x1); x <= uint32(x2); x++ { + for y := uint32(y1); y <= uint32(y2); y++ { + tl.tiles[tileKey{x, y}] = struct{}{} + } } } @@ -130,3 +174,72 @@ func (tl *TileList) Flush() error { // wrote to .tiles~ and now atomically move file to .tiles return os.Rename(fileName, fileName[0:len(fileName)-1]) } + +type bbox struct { + minx, miny, maxx, maxy float64 +} + +func nodesBbox(nodes []element.Node) bbox { + b := bbox{nodes[0].Long, nodes[0].Lat, nodes[0].Long, nodes[0].Lat} + + for i := 1; i < len(nodes); i++ { + if b.maxx < nodes[i].Long { + b.maxx = nodes[i].Long + } + if b.maxy < nodes[i].Lat { + b.maxy = nodes[i].Lat + } + if b.minx > nodes[i].Long { + b.minx = nodes[i].Long + } + if b.miny > nodes[i].Lat { + b.miny = nodes[i].Lat + } + } + return b +} + +func numBboxTiles(b bbox, zoom int) int { + x1, y1 := tileCoord(b.minx, b.maxy, zoom) + x2, y2 := tileCoord(b.maxx, b.miny, zoom) + return int(math.Abs((x2 - x1 + 1) * (y2 - y1 + 1))) +} + +func bresenham(x1, y1, x2, y2 float64) []tileKey { + tiles := make([]tileKey, 0, 4) + steep := false + dx := math.Abs(x2 - x1) + sx := -1.0 + if (x2 - x1) > 0 { + sx = 1.0 + } + dy := math.Abs(y2 - y1) + sy := -1.0 + if (y2 - y1) > 0 { + sy = 1.0 + } + + if dy > dx { + steep = true + x1, y1 = y1, x1 + dx, dy = dy, dx + sx, sy = sy, sx + } + + e := 2*dy - dx + for i := 0.0; i < dx; i++ { + if steep { + tiles = append(tiles, tileKey{X: uint32(y1), Y: uint32(x1)}) + } else { + tiles = append(tiles, tileKey{X: uint32(x1), Y: uint32(y1)}) + } + for e >= 0 { + y1 += sy + e -= 2 * dx + } + x1 += sx + e += 2 * dy + } + tiles = append(tiles, tileKey{X: uint32(x2), Y: uint32(y2)}) + return tiles +} diff --git a/expire/tilelist_test.go b/expire/tilelist_test.go index be5740f..c6b1282 100644 --- a/expire/tilelist_test.go +++ b/expire/tilelist_test.go @@ -2,31 +2,81 @@ package expire import ( "testing" + + "github.com/omniscale/imposm3/element" ) -func TestTileCoords(t *testing.T) { +func TestTileList_ExpireNodes(t *testing.T) { tests := []struct { - long float64 - lat float64 - zoom uint32 - tiles []tileKey + nodes []element.Node + expected int + polygon bool }{ - {0, 0, 14, []tileKey{{8191, 8191}, {8191, 8192}, {8192, 8191}, {8192, 8192}}}, - {0.01, 0, 14, []tileKey{{8192, 8191}, {8192, 8192}}}, - {0, 0.01, 14, []tileKey{{8191, 8191}, {8192, 8191}}}, - {0.01, 0.01, 14, []tileKey{{8192, 8191}}}, - {0.02, 0.01, 14, []tileKey{{8192, 8191}, {8193, 8191}}}, - } + // point + {[]element.Node{{Long: 8.30, Lat: 53.26}}, 1, false}, + // point + paddings + {[]element.Node{{Long: 0, Lat: 0}}, 4, false}, + {[]element.Node{{Long: 0.01, Lat: 0}}, 2, false}, + {[]element.Node{{Long: 0, Lat: 0.01}}, 2, false}, + {[]element.Node{{Long: 0.01, Lat: 0.01}}, 1, false}, + + // line + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.30}, + }, 5, false}, + // same line, but split into multiple segments + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.27}, + {Long: 8.30, Lat: 53.29}, + {Long: 8.30, Lat: 53.30}, + }, 5, false}, + + // L-shape + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.30}, + {Long: 8.35, Lat: 53.30}, + }, 8, false}, + + // closed line (triangle) + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.30}, + {Long: 8.35, Lat: 53.30}, + {Long: 8.30, Lat: 53.25}, + }, 11, false}, + // same closed line but polygon (triangle), whole bbox (4x5 tiles) is expired + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.30}, + {Long: 8.35, Lat: 53.30}, + {Long: 8.30, Lat: 53.25}, + }, 20, true}, + + // large triangle, only outline expired for polygons and lines + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.90}, + {Long: 8.85, Lat: 53.90}, + {Long: 8.30, Lat: 53.25}, + }, 124, true}, + {[]element.Node{ + {Long: 8.30, Lat: 53.25}, + {Long: 8.30, Lat: 53.90}, + {Long: 8.85, Lat: 53.90}, + {Long: 8.30, Lat: 53.25}, + }, 124, false}, + } for _, test := range tests { - actual := TileCoords(test.long, test.lat, test.zoom) - if len(actual) != len(test.tiles) { - t.Errorf("%v != %v", actual, test.tiles) - continue - } - for i := range actual { - if actual[i] != test.tiles[i] { - t.Errorf("%v != %v", actual, test.tiles) + tl := NewTileList(14, "") + tl.ExpireNodes(test.nodes, test.polygon) + if len(tl.tiles) != test.expected { + t.Errorf("expected %d tiles, got %d", test.expected, len(tl.tiles)) + for tk, _ := range tl.tiles { + t.Errorf("\t%v", tk) } } } diff --git a/test/expire_tiles.osc b/test/expire_tiles.osc index 716d9eb..b2d4772 100644 --- a/test/expire_tiles.osc +++ b/test/expire_tiles.osc @@ -105,7 +105,36 @@ + + + + + + + + + + + + + + + - + + + + + + + + + + + + + + + diff --git a/test/expire_tiles.osm b/test/expire_tiles.osm index 74aaf25..8636092 100644 --- a/test/expire_tiles.osm +++ b/test/expire_tiles.osm @@ -124,4 +124,12 @@ + + + + + + + + diff --git a/test/expire_tiles_test.go b/test/expire_tiles_test.go index 24632e0..5a7e58c 100644 --- a/test/expire_tiles_test.go +++ b/test/expire_tiles_test.go @@ -3,7 +3,6 @@ package test import ( "bufio" "database/sql" - "github.com/omniscale/imposm3/expire" "io/ioutil" "os" "path/filepath" @@ -81,49 +80,92 @@ func TestExpireTiles_CheckExpireFile(t *testing.T) { for _, test := range []struct { reason string - long float64 - lat float64 + tiles []tile expire bool }{ - {"create node", 3, 1, true}, - {"modify node (old)", 1, 1, true}, - {"modify node (new)", 1, -1, true}, - {"modify node to unmapped (old)", 4, 1, true}, - {"modify node to unmapped (new)", 4, -1, false}, - {"delete node", 2, 1, true}, + {"create node", []tile{{8328, 8146, 14}}, true}, + {"modify node (old)", []tile{{8237, 8146, 14}}, true}, + {"modify node (new)", []tile{{8237, 8237, 14}}, true}, + {"modify node to unmapped (old)", []tile{{8373, 8146, 14}, {8374, 8146, 14}}, true}, + {"modify node to unmapped (new)", []tile{{8373, 8146, 14}, {8374, 8146, 14}}, false}, + {"delete node", []tile{{8282, 8146, 14}, {8283, 8146, 14}}, true}, - {"delete way", 2.0001, 2, true}, - {"modify way", 1.0001, 2, true}, - {"modify way from node (old)", 3.0001, 2, true}, - {"modify way from node (new)", 3.0001, -2, true}, - {"create way", 4.0001, 2, true}, + {"delete way", []tile{{8283, 8100, 14}}, true}, + {"modify way", []tile{{8237, 8100, 14}}, true}, + {"modify way from node (old)", []tile{{8328, 8100, 14}}, true}, + {"modify way from node (new)", []tile{{8328, 8283, 14}}, true}, + {"create way", []tile{{8374, 8100, 14}}, true}, + {"create long way", []tile{{8419, 8100, 14}, {8420, 8100, 14}, {8421, 8100, 14}}, true}, - {"create long way (start)", 5.00, 2, true}, - {"create long way (mid)", 5.025, 2, false}, // TODO not implemented - {"create long way (end)", 5.05, 2, true}, + {"modify relation", []tile{{8237, 8055, 14}}, true}, + {"delete relation", []tile{{8283, 8055, 14}}, true}, + {"modify relation from way", []tile{{8328, 8055, 14}}, true}, + {"modify relation from nodes (old)", []tile{{8374, 8055, 14}}, true}, + {"modify relation from nodes (new)", []tile{{8374, 8328, 14}}, true}, + {"create polygon (box)", []tile{ + {8237, 8007, 14}, + {8237, 8008, 14}, + {8237, 8009, 14}, + {8238, 8007, 14}, + {8238, 8008, 14}, + {8238, 8009, 14}, + {8239, 8007, 14}, + {8239, 8008, 14}, + {8239, 8009, 14}, + }, true}, - {"modify relation", 1.0001, 3, true}, - {"delete relation", 2.0001, 3, true}, - {"modify relation from way", 3.0001, 3, true}, - {"modify relation from nodes (old)", 4.0001, 3, true}, - {"modify relation from nodes (new)", 4.0001, -3, true}, + {"create polygon (outline)", []tile{ + {8310, 8005, 14}, {8302, 7991, 14}, {8283, 7993, 14}, + {8300, 8009, 14}, {8283, 8003, 14}, {8308, 8009, 14}, + {8310, 7995, 14}, {8285, 8009, 14}, {8288, 8009, 14}, + {8301, 8009, 14}, {8310, 8002, 14}, {8302, 8009, 14}, + {8310, 8003, 14}, {8286, 8009, 14}, {8300, 7991, 14}, + {8283, 7994, 14}, {8296, 8009, 14}, {8298, 8009, 14}, + {8310, 8009, 14}, {8283, 7999, 14}, {8283, 7992, 14}, + {8290, 7991, 14}, {8305, 8009, 14}, {8309, 7991, 14}, + {8306, 7991, 14}, {8291, 7991, 14}, {8283, 7996, 14}, + {8310, 7996, 14}, {8293, 7991, 14}, {8310, 8007, 14}, + {8310, 8001, 14}, {8307, 8009, 14}, {8299, 8009, 14}, + {8310, 7998, 14}, {8310, 7999, 14}, {8301, 7991, 14}, + {8283, 7998, 14}, {8283, 8006, 14}, {8289, 8009, 14}, + {8310, 8008, 14}, {8285, 7991, 14}, {8283, 8002, 14}, + {8289, 7991, 14}, {8286, 7991, 14}, {8288, 7991, 14}, + {8283, 8008, 14}, {8283, 8005, 14}, {8310, 7992, 14}, + {8310, 8004, 14}, {8310, 7991, 14}, {8296, 7991, 14}, + {8292, 7991, 14}, {8283, 8009, 14}, {8291, 8009, 14}, + {8293, 8009, 14}, {8284, 8009, 14}, {8287, 7991, 14}, + {8297, 8009, 14}, {8283, 8007, 14}, {8299, 7991, 14}, + {8310, 7997, 14}, {8303, 8009, 14}, {8290, 8009, 14}, + {8306, 8009, 14}, {8283, 7995, 14}, {8283, 8000, 14}, + {8295, 8009, 14}, {8310, 8006, 14}, {8304, 8009, 14}, + {8295, 7991, 14}, {8292, 8009, 14}, {8309, 8009, 14}, + {8283, 8004, 14}, {8307, 7991, 14}, {8305, 7991, 14}, + {8283, 8001, 14}, {8284, 7991, 14}, {8297, 7991, 14}, + {8310, 7993, 14}, {8303, 7991, 14}, {8294, 8009, 14}, + {8287, 8009, 14}, {8283, 7991, 14}, {8283, 7997, 14}, + {8308, 7991, 14}, {8304, 7991, 14}, {8298, 7991, 14}, + {8310, 8000, 14}, {8310, 7994, 14}, {8294, 7991, 14}, + }, true}, } { - for _, coord := range expire.TileCoords(test.long, test.lat, 14) { - x, y := coord.X, coord.Y + + for _, coord := range test.tiles { if test.expire { - if _, ok := tiles[tile{x: int(x), y: int(y), z: 14}]; !ok { - t.Errorf("missing expire tile for %s 14/%d/%d for %f %f", test.reason, x, y, test.long, test.lat) + if _, ok := tiles[coord]; !ok { + t.Errorf("missing expire tile for %s %v", test.reason, coord) } else { - delete(tiles, tile{x: int(x), y: int(y), z: 14}) + delete(tiles, coord) } } else { - if _, ok := tiles[tile{x: int(x), y: int(y), z: 14}]; ok { - t.Errorf("found expire tile for %s 14/%d/%d for %f %f", test.reason, x, y, test.long, test.lat) + if _, ok := tiles[coord]; ok { + t.Errorf("found expire tile for %s %v", test.reason, coord) } } } } + if len(tiles) > 0 { + t.Errorf("found %d unexpected tiles", len(tiles)) + } for tile, _ := range tiles { t.Errorf("unexpected tile expired: %v", tile) }