Skip to content

Commit

Permalink
feat(server): add geocondig api to govpolygon
Browse files Browse the repository at this point in the history
  • Loading branch information
rot1024 committed Mar 28, 2024
1 parent 93b2b33 commit 2dfeab9
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 1 deletion.
1 change: 1 addition & 0 deletions server/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ go 1.21

require (
github.com/99designs/gqlgen v0.17.43
github.com/JamesLMilner/quadtree-go v0.0.0-20191212211504-d12870ffe403
github.com/dustin/go-humanize v1.0.1
github.com/eukarya-inc/jpareacode v1.0.1-0.20240314080116-ae89cfd85c6a
github.com/go-playground/validator/v10 v10.16.0
Expand Down
2 changes: 2 additions & 0 deletions server/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapp
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.45.0/go.mod h1:qkFPtMouQjW5ugdHIOthiTbweVHUTqbS0Qsu55KqXks=
github.com/HdrHistogram/hdrhistogram-go v1.1.2 h1:5IcZpTvzydCQeHzK4Ef/D5rrSqwxob0t8PQPMybUNFM=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/JamesLMilner/quadtree-go v0.0.0-20191212211504-d12870ffe403 h1:d0humYgeOKZG99ab6SQl+sDsO6W9Pl5UuO515QT/IxQ=
github.com/JamesLMilner/quadtree-go v0.0.0-20191212211504-d12870ffe403/go.mod h1:53pPERNR0+5DxLnYw7RCA9DS+cyETfT/knWWoNL/F1s=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d h1:licZJFw2RwpHMqeKTCYkitsPqHNxTmd4SNR5r94FGM8=
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d/go.mod h1:asat636LX7Bqt5lYEZ27JNDcqxfjdBQuJ/MM4CN/Lzo=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
Expand Down
61 changes: 60 additions & 1 deletion server/govpolygon/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,17 @@ import (
"io"
"net/http"
"path/filepath"
"strconv"
"sync"
"time"

"github.com/eukarya-inc/jpareacode"
"github.com/eukarya-inc/jpareacode/jpareacodepref"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"github.com/reearth/reearthx/log"
"github.com/reearth/reearthx/util"
"github.com/samber/lo"
)

const dirpath = "govpolygondata"
Expand All @@ -28,6 +32,7 @@ type Handler struct {
httpClient *http.Client
lock sync.RWMutex
geojson []byte
qt *Quadtree
updateIfNotExists bool
updatedAt time.Time
}
Expand All @@ -44,16 +49,21 @@ func New(gqlEndpoint string, updateIfNotExists bool) *Handler {
func (h *Handler) Route(g *echo.Group) *Handler {
g.Use(middleware.CORS(), middleware.Gzip())
g.GET("/plateaugovs.geojson", h.GetGeoJSON)
g.GET("/geocoding", h.FindCodeFromLngLat)
// g.GET("/update", h.Update, errorLogger)
return h
}

func (h *Handler) GetGeoJSON(c echo.Context) error {
func (h *Handler) updateIfNeed(c echo.Context) {
if h.updateIfNotExists && h.geojson == nil {
if err := h.Update(c); err != nil {
log.Errorfc(c.Request().Context(), "govpolygon: fail to init: %v", err)
}
}
}

func (h *Handler) GetGeoJSON(c echo.Context) error {
h.updateIfNeed(c)

h.lock.RLock()
defer h.lock.RUnlock()
Expand Down Expand Up @@ -98,6 +108,8 @@ func (h *Handler) Update(c echo.Context) error {
return fmt.Errorf("failed to marshal geojson: %w", err)
}

h.qt = NewQuadtree(g.Features)

if !initial {
h.lock.Lock()
defer h.lock.Unlock()
Expand Down Expand Up @@ -198,3 +210,50 @@ func (h *Handler) getCityNames(ctx context.Context) ([]string, error) {

return names, nil
}

func (h *Handler) FindCodeFromLngLat(c echo.Context) error {
h.updateIfNeed(c)

lngs, lats := c.QueryParam("lng"), c.QueryParam("lat")
if lngs == "" || lats == "" {
return c.JSON(http.StatusBadRequest, "lng and lat are required")
}

lng, err := strconv.ParseFloat(lngs, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, "invalid lng")
}

lat, err := strconv.ParseFloat(lats, 64)
if err != nil {
return c.JSON(http.StatusBadRequest, "invalid lat")
}

h.lock.RLock()
defer h.lock.RUnlock()
if h.qt == nil {
return c.JSON(http.StatusNotFound, "not found")
}

code, _ := h.qt.Find(lng, lat)
city := jpareacode.CityByCodeString(code)

if city == nil {
return c.JSON(http.StatusOK, map[string]any{
"lng": lng,
"lat": lat,
})
}

return c.JSON(http.StatusOK, map[string]any{
"lng": lng,
"lat": lat,
"pref": jpareacodepref.PrefectureNameByCodeInt(city.PrefCode),
"prefCode": jpareacodepref.FormatPrefectureCode(city.PrefCode),
"city": lo.EmptyableToPtr(city.CityName),
"cityCode": lo.EmptyableToPtr(jpareacode.FormatCityCode(city.CityCode)),
"ward": lo.EmptyableToPtr(city.WardName),
"wardCode": lo.EmptyableToPtr(jpareacode.FormatCityCode(city.WardCode)),
"code": jpareacode.FormatCityCode(city.Code()),
})
}
8 changes: 8 additions & 0 deletions server/govpolygon/processor.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,14 @@ func (p *Processor) ComputeGeoJSON(ctx context.Context, names []string) (*geojso
}

func computeGeojsonFeatures(features []*geojson.Feature, names []string) (*geojson.FeatureCollection, []string) {
if len(names) == 0 {
fc := geojson.NewFeatureCollection()
for _, f := range features {
fc.AddFeature(f)
}
return fc, nil
}

nameMap := map[string]struct{}{}
notfound := map[string]struct{}{}
citiesWithWards := map[string]struct{}{}
Expand Down
97 changes: 97 additions & 0 deletions server/govpolygon/quadtree.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package govpolygon

import (
"github.com/JamesLMilner/quadtree-go"
geojson "github.com/paulmach/go.geojson"
)

type Quadtree struct {
qt *quadtree.Quadtree
ft map[quadtree.Bounds]string
}

func NewQuadtree(f []*geojson.Feature) *Quadtree {
ft := map[quadtree.Bounds]string{}
qt := &quadtree.Quadtree{
MaxObjects: 10,
MaxLevels: 100,
Objects: make([]quadtree.Bounds, 0),
Nodes: make([]quadtree.Quadtree, 0),
}

for _, f := range f {
b, ok := bounds(f.Geometry)
if !ok {
continue
}

qt.Insert(b)
ft[b] = f.Properties["code"].(string)
}

return &Quadtree{
qt: qt,
ft: ft,
}
}

func (q *Quadtree) Find(lng, lat float64) (string, bool) {
res := q.qt.RetrieveIntersections(quadtree.Bounds{
X: lng,
Y: lat,
})
if len(res) == 0 {
return "", false
}

r, ok := q.ft[res[0]]
return r, ok
}

func bounds(g *geojson.Geometry) (b quadtree.Bounds, _ bool) {
if !g.IsMultiPolygon() && !g.IsPolygon() {
return
}

if g.IsPolygon() {
g = &geojson.Geometry{
Type: "MultiPolygon",
MultiPolygon: [][][][]float64{g.Polygon},
}
}

minlat := -1.0
minlng := -1.0
maxlat := -1.0
maxlng := -1.0

for _, polygon := range g.MultiPolygon {
for _, ring := range polygon {
for _, p := range ring {
lng := p[0]
lat := p[1]

if minlat == -1 || lat < minlat {
minlat = lat
}
if minlng == -1 || lng < minlng {
minlng = lng
}

if maxlat == -1 || lat > maxlat {
maxlat = lat
}
if maxlng == -1 || lng > maxlng {
maxlng = lng
}
}
}
}

return quadtree.Bounds{
X: minlng,
Y: minlat,
Width: maxlng - minlng,
Height: maxlat - minlat,
}, true
}
33 changes: 33 additions & 0 deletions server/govpolygon/quadtree_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package govpolygon

import (
"context"
"path/filepath"
"testing"

"github.com/stretchr/testify/assert"
)

func TestQuadtree(t *testing.T) {
p := NewProcessor(filepath.Join(dirpath, "japan_city.geojson"))
ctx := context.Background()
f, _, err := p.ComputeGeoJSON(ctx, nil)
assert.NoError(t, err)

q := NewQuadtree(f.Features)
res, ok := q.Find(139.760296, 35.686067)
assert.True(t, ok)
assert.Equal(t, "13101", res)
}

func BenchmarkQuadtree(b *testing.B) {
p := NewProcessor(filepath.Join(dirpath, "japan_city.geojson"))
ctx := context.Background()
f, _, _ := p.ComputeGeoJSON(ctx, nil)
q := NewQuadtree(f.Features)

b.ResetTimer()
for i := 0; i < b.N; i++ {
_, _ = q.Find(139.760296, 35.686067)
}
}

0 comments on commit 2dfeab9

Please sign in to comment.