diff --git a/draw.go b/draw.go index c8408c6..e8287a9 100644 --- a/draw.go +++ b/draw.go @@ -5,59 +5,112 @@ import ( "image" "image/color" "image/draw" - "math" "time" - "code.google.com/p/draw2d/draw2d" - "code.google.com/p/freetype-go/freetype" - "code.google.com/p/freetype-go/freetype/truetype" + "github.com/golang/geo/s2" + "github.com/llgcode/draw2d/draw2dimg" ) const tileSize = 256 -func DrawTile(nwPt, sePt Pointer, zoom int64, font *truetype.Font, data *OsmData, debug bool) (image.Image, error) { +func DrawTile(nwPt, sePt Pointer, zoom int64, data *OsmData, debug bool) (image.Image, error) { t := time.Now() + // s2.Points of tile 4 vertex + p1 := s2.PointFromLatLng(s2.LatLngFromDegrees(nwPt.Lat(), nwPt.Lon())) + p2 := s2.PointFromLatLng(s2.LatLngFromDegrees(sePt.Lat(), nwPt.Lon())) + p3 := s2.PointFromLatLng(s2.LatLngFromDegrees(nwPt.Lat(), sePt.Lon())) + p4 := s2.PointFromLatLng(s2.LatLngFromDegrees(sePt.Lat(), sePt.Lon())) // Create white image img := image.NewRGBA(image.Rect(0, 0, tileSize, tileSize)) draw.Draw(img, img.Bounds(), image.White, image.ZP, draw.Src) // Plot some features - for fName, features := range data.Features { - if mapFeatures[fName].MinZoom > zoom { + for _, feature := range data.Findex.GetFeatures(nwPt, sePt, zoom) { + if mz, ok := mapFeatures[feature.FName]; ok { + if mz.MinZoom > zoom { + continue + } + } else { continue } - for _, feature := range features { - switch feature.Type { - case ItemTypeNode: - //TODO - case ItemTypeWay: - way := data.Ways[feature.Id] - //TODO: this ignores ways crossing over the tile - withinBounds := false - for _, node := range way.GetNodes(data.Nodes) { + switch feature.Type { + case ItemTypeNode: + //TODO + case ItemTypeWay: + way := data.Ways[feature.Id] + var ( + // previous plotting point, nil if already added to coords or first time + prevCoords []float64 + // previous s2 Point. Always present, except first time + prevS2Point s2.Point + ) + coords := [][]float64{} + prevWithinBounds := false + first := true + for _, node := range way.GetNodes(data.Nodes) { + if first { + first = false + x, y := getRelativeXY(nwPt, node, float64(zoom)) if node.Lon() > nwPt.Lon() && node.Lon() < sePt.Lon() && node.Lat() < nwPt.Lat() && node.Lat() > sePt.Lat() { - withinBounds = true + coords = append(coords, []float64{x, y}) + prevWithinBounds = true + } else { + //x, y := getRelativeXY(nwPt, node, float64(zoom)) + //a1 := s2.PointFromLatLng(s2.LatLngFromDegrees(node.Lat(), node.Lon())) + prevS2Point = s2.PointFromLatLng(s2.LatLngFromDegrees(node.Lat(), node.Lon())) + prevCoords = []float64{x, y} + prevWithinBounds = false } - } - - if !withinBounds { continue } - coords := [][]float64{} - for _, node := range way.GetNodes(data.Nodes) { + if node.Lon() > nwPt.Lon() && node.Lon() < sePt.Lon() && + node.Lat() < nwPt.Lat() && node.Lat() > sePt.Lat() { + if prevWithinBounds == false { + if len(prevCoords) > 0 { + coords = append(coords, prevCoords) + } + } x, y := getRelativeXY(nwPt, node, float64(zoom)) coords = append(coords, []float64{x, y}) + prevWithinBounds = true + prevCoords = nil + } else { + x, y := getRelativeXY(nwPt, node, float64(zoom)) + a1 := s2.PointFromLatLng(s2.LatLngFromDegrees(node.Lat(), node.Lon())) + if prevWithinBounds == true { + coords = append(coords, []float64{x, y}) + prevCoords = nil //already added to coords + } else { + + if s2.SimpleCrossing(p1, p2, a1, prevS2Point) || s2.SimpleCrossing(p1, p3, a1, prevS2Point) || s2.SimpleCrossing(p2, p4, a1, prevS2Point) { + if len(prevCoords) > 0 { + coords = append(coords, prevCoords) + } + coords = append(coords, []float64{x, y}) + prevCoords = nil + } else { + if len(coords) > 0 { + drawPolyLine(img, color.Black, coords) + coords = coords[:0] + } + prevCoords = []float64{x, y} + } + } + prevS2Point = a1 + prevWithinBounds = false } + } + if len(coords) > 0 { drawPolyLine(img, color.Black, coords) - - case ItemTypeRelation: - //TODO } + + case ItemTypeRelation: + //TODO } } @@ -72,13 +125,13 @@ func DrawTile(nwPt, sePt Pointer, zoom int64, font *truetype.Font, data *OsmData } // Tile location - err := drawText(img, font, red, tileSize/2, 20, fmt.Sprintf("%f, %f", nwPt.Lon(), nwPt.Lat())) + err := drawText(img, red, tileSize/2, 20, fmt.Sprintf("%f, %f", nwPt.Lon(), nwPt.Lat())) if err != nil { return nil, err } // Tile location - err = drawText(img, font, red, tileSize/2, tileSize-20, time.Since(t).String()) + err = drawText(img, red, tileSize/2, tileSize-20, time.Since(t).String()) if err != nil { return nil, err } @@ -87,54 +140,23 @@ func DrawTile(nwPt, sePt Pointer, zoom int64, font *truetype.Font, data *OsmData return img, nil } -func drawText(img *image.RGBA, font *truetype.Font, color color.Color, x, y int, s string) error { - var ptSize float64 = 12 - - ctx := freetype.NewContext() - ctx.SetDPI(72) - ctx.SetFont(font) - ctx.SetFontSize(ptSize) - ctx.SetClip(img.Bounds()) - ctx.SetDst(img) - ctx.SetSrc(image.NewUniform(color)) - ctx.SetHinting(freetype.FullHinting) - - width := int(widthOfString(font, ptSize, s)) - pt := freetype.Pt(x-width/2, y+int(ctx.PointToFix32(ptSize)>>8)/2) - _, err := ctx.DrawString(s, pt) - if err != nil { - return err - } +func drawText(img *image.RGBA, cc color.Color, x, y int, s string) error { + + path := draw2dimg.NewGraphicContext(img) + path.SetStrokeColor(cc) + path.SetFillColor(cc) + path.SetDPI(72) + path.StrokeStringAt(s, float64(x), float64(y)) return nil } -// https://code.google.com/p/plotinum/source/browse/vg/font.go#160 -func widthOfString(font *truetype.Font, size float64, s string) float64 { - // scale converts truetype.FUnit to float64 - scale := size / float64(font.FUnitsPerEm()) - - width := 0 - prev, hasPrev := truetype.Index(0), false - for _, rune := range s { - index := font.Index(rune) - if hasPrev { - width += int(font.Kerning(font.FUnitsPerEm(), prev, index)) - } - width += int(font.HMetric(font.FUnitsPerEm(), index).AdvanceWidth) - prev, hasPrev = index, true - } - - return float64(width) * scale -} +func drawPolyLine(img *image.RGBA, cc color.Color, coords [][]float64) { + path := draw2dimg.NewGraphicContext(img) -func round(n float64) int { - //TODO: this is incorrect - return int(math.Floor(n)) -} + path.SetStrokeColor(cc) + path.SetLineWidth(1) -func drawPolyLine(img *image.RGBA, color color.Color, coords [][]float64) { - path := draw2d.NewPathStorage() for i, coord := range coords { if i == 0 { path.MoveTo(coord[0], coord[1]) @@ -143,7 +165,7 @@ func drawPolyLine(img *image.RGBA, color color.Color, coords [][]float64) { } } - gc := draw2d.NewGraphicContext(img) - gc.SetStrokeColor(color) - gc.Stroke(path) + // TODO check area tag ? + // path.Close() + path.Stroke() } diff --git a/draw_test.go b/draw_test.go index 074680e..0887e05 100644 --- a/draw_test.go +++ b/draw_test.go @@ -1,10 +1,7 @@ package tiles import ( - "io/ioutil" "testing" - - "code.google.com/p/freetype-go/freetype" ) func BenchmarkDrawTile(b *testing.B) { @@ -12,17 +9,6 @@ func BenchmarkDrawTile(b *testing.B) { sePt := Point{-4.471435546875, 54.156001090284924} scale := int64(15) - // Read font - font_, err := ioutil.ReadFile("example/FiraSans-Regular.ttf") - if err != nil { - b.Fatalf("Benchmark setup failed: %#v\n", err) - } - - font, err := freetype.ParseFont(font_) - if err != nil { - b.Fatalf("Benchmark setup failed: %#v\n", err) - } - // Read PBF file data, err := ParsePbf("example/isle-of-man-latest.osm.pbf") if err != nil { @@ -32,7 +18,7 @@ func BenchmarkDrawTile(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - _, err := DrawTile(nwPt, sePt, scale, font, data, false) + _, err := DrawTile(nwPt, sePt, scale, data, false) if err != nil { b.Fatalf("Received error: %#v\n", err) } diff --git a/example/FiraSans-Regular.ttf b/example/FiraSans-Regular.ttf deleted file mode 100644 index a4e6563..0000000 Binary files a/example/FiraSans-Regular.ttf and /dev/null differ diff --git a/example/luxisr.ttf b/example/luxisr.ttf new file mode 100644 index 0000000..c47fd20 Binary files /dev/null and b/example/luxisr.ttf differ diff --git a/example/main.go b/example/main.go index 5a68ef4..946ba4a 100644 --- a/example/main.go +++ b/example/main.go @@ -8,6 +8,7 @@ import ( "strconv" "github.com/holizz/go-tile-server" + "github.com/llgcode/draw2d" ) func main() { @@ -16,7 +17,8 @@ func main() { port = 3000 } - tileHandler := tiles.NewTileHandler("/tiles", "isle-of-man-latest.osm.pbf", "FiraSans-Regular.ttf") + draw2d.SetFontFolder(".") + tileHandler := tiles.NewTileHandler("/tiles", "isle-of-man-latest.osm.pbf") http.Handle("/tiles/", tileHandler) http.Handle("/", http.FileServer(http.Dir("public"))) diff --git a/features.go b/features.go index 4966a3b..0cd13ed 100644 --- a/features.go +++ b/features.go @@ -16,8 +16,56 @@ var mapFeatures = map[string]Feature{ {"natural", "coastline"}, }, }, - "all-other-roads": { + "borders2": { + MinZoom: 0, + Tags: []Tag{ + {"admin_level", "2"}, + }, + }, + "borders3": { + MinZoom: 6, + Tags: []Tag{ + {"admin_level", "3"}, + }, + }, + "borders4": { + MinZoom: 8, + Tags: []Tag{ + {"admin_level", "4"}, + }, + }, + "borders5": { + MinZoom: 10, + Tags: []Tag{ + {"admin_level", "5"}, + }, + }, + "borders6": { + MinZoom: 12, + Tags: []Tag{ + {"admin_level", "6"}, + }, + }, + "borders7": { + MinZoom: 12, + Tags: []Tag{ + {"admin_level", "7"}, + }, + }, + "borders8": { + MinZoom: 13, + Tags: []Tag{ + {"admin_level", "8"}, + }, + }, + "borders9": { MinZoom: 14, + Tags: []Tag{ + {"admin_level", "9"}, + }, + }, + "all-other-roads": { + MinZoom: 12, Tags: []Tag{ {"highway", "unclassified"}, {"highway", "residential"}, @@ -35,7 +83,7 @@ var mapFeatures = map[string]Feature{ }, }, "major-ish-roads": { - MinZoom: 12, + MinZoom: 9, Tags: []Tag{ {"highway", "primary"}, {"highway", "secondary"}, @@ -43,7 +91,7 @@ var mapFeatures = map[string]Feature{ }, }, "major-major-roads": { - MinZoom: 10, + MinZoom: 6, Tags: []Tag{ {"highway", "motorway"}, {"highway", "trunk"}, diff --git a/http.go b/http.go index d0739d5..c1826b5 100644 --- a/http.go +++ b/http.go @@ -2,37 +2,19 @@ package tiles import ( "image/png" - "io/ioutil" "log" "net/http" "strconv" "strings" - - "code.google.com/p/freetype-go/freetype" - "code.google.com/p/freetype-go/freetype/truetype" ) type TileHandler struct { prefix string - font *truetype.Font data *OsmData } // prefix should be of the form "/tiles" (without the trailing slash) -func NewTileHandler(prefix, pbfPath, fontPath string) *TileHandler { - // Read font - log.Println("Parsing font file...") - font_, err := ioutil.ReadFile(fontPath) - if err != nil { - panic(err) - } - - font, err := freetype.ParseFont(font_) - if err != nil { - panic(err) - } - log.Println("Parsing font file... [DONE]") - +func NewTileHandler(prefix, pbfPath string) *TileHandler { // Read PBF log.Println("Parsing PBF file...") osmData, err := ParsePbf(pbfPath) @@ -43,7 +25,6 @@ func NewTileHandler(prefix, pbfPath, fontPath string) *TileHandler { return &TileHandler{ prefix: prefix, - font: font, data: osmData, } } @@ -86,7 +67,7 @@ func (th *TileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { nwPt := getLonLatFromTileName(x, y, zoom) sePt := getLonLatFromTileName(x+1, y+1, zoom) - img, err := DrawTile(nwPt, sePt, zoom, th.font, th.data, debug) + img, err := DrawTile(nwPt, sePt, zoom, th.data, debug) if err != nil { panic(err) } diff --git a/pbf.go b/pbf.go index 1b2da83..5b39df5 100644 --- a/pbf.go +++ b/pbf.go @@ -3,6 +3,7 @@ package tiles import ( "fmt" "io" + "log" "os" "github.com/qedus/osmpbf" @@ -17,14 +18,15 @@ const ( ) type OsmData struct { - Nodes map[int64]Node - Ways map[int64]Way - Features map[string][]FeatureRef + Nodes map[int64]Node + Ways map[int64]Way + Findex S2Index } type FeatureRef struct { - Id int64 - Type ItemType + Id int64 + Type ItemType + FName string } type Node struct { @@ -99,9 +101,9 @@ func ParsePbf(path string) (*OsmData, error) { } data := &OsmData{ - Nodes: map[int64]Node{}, - Ways: map[int64]Way{}, - Features: map[string][]FeatureRef{}, + Nodes: map[int64]Node{}, + Ways: map[int64]Way{}, + Findex: make(S2Index), } for { @@ -119,7 +121,7 @@ func ParsePbf(path string) (*OsmData, error) { fName, ok := way.MatchAny(mapFeatures) if ok { data.Ways[way.Id] = way - data.Features[fName] = append(data.Features[fName], FeatureRef{way.Id, ItemTypeWay}) + data.Findex.AddWay(way, fName, data) } case *osmpbf.Relation: // Ignore @@ -129,5 +131,8 @@ func ParsePbf(path string) (*OsmData, error) { } } + log.Println("Num s2Cells", len(data.Findex)) + log.Println("Num ways", len(data.Ways)) + log.Println("Num nodes", len(data.Nodes)) return data, nil } diff --git a/s2.go b/s2.go new file mode 100644 index 0000000..9012c70 --- /dev/null +++ b/s2.go @@ -0,0 +1,103 @@ +package tiles + +import ( + //"fmt" + "github.com/golang/geo/s2" +) + +type S2Index map[s2.CellID]([]FeatureRef) + +func (si S2Index) AddWay(w Way, fname string, data *OsmData) { + c := s2.EmptyCap() + + for _, node := range w.GetNodes(data.Nodes) { + c = c.AddPoint(s2.PointFromLatLng(s2.LatLngFromDegrees(node.Lat(), node.Lon()))) + } + + if c.IsEmpty() { + return + } + + rc := &s2.RegionCoverer{MaxLevel: 30, MaxCells: 10} + cu := rc.FastCovering(c) + for _, cid := range cu { + si[cid] = append(si[cid], FeatureRef{w.Id, ItemTypeWay, fname}) + for l := cid.Level(); l > 0; l-- { + cid = cid.Parent(l - 1) + if _, ok := si[cid]; !ok { + si[cid] = make([]FeatureRef, 0) + } + } + } +} + +func (si S2Index) GetFeatures(nwPt, sePt Pointer, zoom int64) []FeatureRef { + + r := s2.RectFromLatLng(s2.LatLngFromDegrees(nwPt.Lat(), nwPt.Lon())) + r = r.AddPoint(s2.LatLngFromDegrees(sePt.Lat(), sePt.Lon())) + + rc := &s2.RegionCoverer{MaxLevel: 30, MaxCells: 10} + + cu := rc.Covering(r) + + visitCid := make(map[s2.CellID]bool) + visitF := make(map[int64]bool) + ret := make([]FeatureRef, 0) + + for _, cid := range cu { + if v, ok := si[cid]; ok { + ret = si.VisitDown(cid, v, visitF, ret) + + } + for l := cid.Level(); l > 0; l-- { + cid = cid.Parent(l - 1) + ret = si.VisitUp(cid, visitCid, visitF, ret) + + } + } + //fmt.Println( len(ret)) + return ret +} + +func (si S2Index) VisitUp(cid s2.CellID, visitCid map[s2.CellID]bool, visitF map[int64]bool, ret []FeatureRef) []FeatureRef { + v, ok := si[cid] + if !ok { + return ret + } + if visitCid[cid] { + return ret + } + visitCid[cid] = true + + for _, f := range v { + if !visitF[f.Id] { + ret = append(ret, f) + visitF[f.Id] = true + } + } + return ret +} + +func (si S2Index) VisitDown(cid s2.CellID, fr []FeatureRef, visitF map[int64]bool, ret []FeatureRef) []FeatureRef { + + for _, f := range fr { + if !visitF[f.Id] { + ret = append(ret, f) + visitF[f.Id] = true + } + } + + if !cid.IsLeaf() { + chs := cid.Children() + for i := 0; i < 4; i++ { + if v, ok := si[chs[i]]; ok { + ret = si.VisitDown(chs[i], v, visitF, ret) + + } + } + + } + + return ret + +}