Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GeoJSONL support #185

Merged
merged 2 commits into from
Oct 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions server/rest/agency_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rest

import (
"context"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -163,6 +164,42 @@ func TestAgencyRequest(t *testing.T) {
}
}

func TestAgencyRequest_Format(t *testing.T) {
tcs := []testRest{
{
name: "agency geojson",
format: "geojson",
h: AgencyRequest{FeedOnestopID: "CT", WithCursor: WithCursor{Limit: 1}},
f: func(t *testing.T, jj string) {
a := gjson.Get(jj, "features").Array()
assert.Equal(t, 1, len(a))
assert.Equal(t, "Feature", gjson.Get(jj, "features.0.type").String())
assert.Equal(t, "Polygon", gjson.Get(jj, "features.0.geometry.type").String())
assert.Equal(t, "CT", gjson.Get(jj, "features.0.properties.feed_version.feed.onestop_id").String())
assert.Greater(t, gjson.Get(jj, "meta.after").Int(), int64(0))
},
},
{
name: "agency geojsonl",
format: "geojsonl",
h: AgencyRequest{FeedOnestopID: "CT", WithCursor: WithCursor{Limit: 1}},
f: func(t *testing.T, jj string) {
split := strings.Split(jj, "\n")
assert.Equal(t, 1, len(split))
assert.Equal(t, "Feature", gjson.Get(split[0], "type").String())
assert.Equal(t, "Polygon", gjson.Get(split[0], "geometry.type").String())
assert.Equal(t, "CT", gjson.Get(split[0], "properties.feed_version.feed.onestop_id").String())
},
},
}
srv, te := testRestConfig(t)
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
testquery(t, srv, te, tc)
})
}
}

func TestAgencyRequest_Pagination(t *testing.T) {
srv, te := testRestConfig(t)
allEnts, err := te.Finder.FindAgencies(context.Background(), nil, nil, nil, nil)
Expand Down
39 changes: 39 additions & 0 deletions server/rest/feed_request_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package rest

import (
"strings"
"testing"

"github.com/interline-io/transitland-server/model"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)

func TestFeedRequest(t *testing.T) {
Expand Down Expand Up @@ -161,6 +164,42 @@ func TestFeedRequest(t *testing.T) {
}
}

func TestFeedRequest_Format(t *testing.T) {
tcs := []testRest{
{
name: "feed geojson",
format: "geojson",
h: FeedRequest{WithCursor: WithCursor{Limit: 5}},
f: func(t *testing.T, jj string) {
a := gjson.Get(jj, "features").Array()
assert.Equal(t, 5, len(a))
assert.Equal(t, "Feature", gjson.Get(jj, "features.0.type").String())
assert.Equal(t, "Polygon", gjson.Get(jj, "features.0.geometry.type").String())
assert.Equal(t, "CT", gjson.Get(jj, "features.0.properties.onestop_id").String())
assert.Greater(t, gjson.Get(jj, "meta.after").Int(), int64(0))
},
},
{
name: "feed geojsonl",
format: "geojsonl",
h: FeedRequest{WithCursor: WithCursor{Limit: 5}},
f: func(t *testing.T, jj string) {
split := strings.Split(jj, "\n")
assert.Equal(t, 5, len(split))
assert.Equal(t, "Feature", gjson.Get(split[0], "type").String())
assert.Equal(t, "Polygon", gjson.Get(split[0], "geometry.type").String())
assert.Equal(t, "CT", gjson.Get(split[0], "properties.onestop_id").String())
},
},
}
srv, te := testRestConfig(t)
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
testquery(t, srv, te, tc)
})
}
}

func TestFeedRequest_License(t *testing.T) {
testcases := []testRest{
{
Expand Down
6 changes: 4 additions & 2 deletions server/rest/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,8 +112,10 @@ func processGeoJSON(ent apiHandler, response map[string]interface{}) error {
}
geometry := f["geometry"]
if geometry == nil {
log.Infof("feature has no geometry, skipping")
continue
geometry = map[string]any{
"type": "Polygon",
"coordinates": []float64{},
}
}
delete(f, "geometry")
properties := hw{}
Expand Down
1 change: 1 addition & 0 deletions server/rest/operator_request.gql
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ query ($limit: Int, $after: Int, $include_alerts: Boolean!, $where: OperatorFilt
id
agency_id
agency_name
geometry
alerts @include(if: $include_alerts) {
...alert
}
Expand Down
26 changes: 24 additions & 2 deletions server/rest/rest.go
Original file line number Diff line number Diff line change
Expand Up @@ -331,7 +331,7 @@ func makeRequest(ctx context.Context, cfg restConfig, ent apiHandler, format str
}
}

if format == "geojson" || format == "png" {
if format == "geojson" || format == "geojsonl" || format == "png" {
// TODO: Don't process response in-place.
if v, ok := ent.(canProcessGeoJSON); ok {
if err := v.ProcessGeoJSON(response); err != nil {
Expand All @@ -342,7 +342,9 @@ func makeRequest(ctx context.Context, cfg restConfig, ent apiHandler, format str
return nil, err
}
}
if format == "png" {
if format == "geojsonl" {
return renderGeojsonl(response)
} else if format == "png" {
b, err := json.Marshal(response)
if err != nil {
return nil, err
Expand All @@ -353,6 +355,26 @@ func makeRequest(ctx context.Context, cfg restConfig, ent apiHandler, format str
return json.Marshal(response)
}

func renderGeojsonl(response map[string]any) ([]byte, error) {
var ret []byte
feats, ok := response["features"].([]map[string]any)
if !ok {
return nil, errors.New("not features")
}
for i, feat := range feats {
j, err := json.Marshal(feat)
if err != nil {
return nil, err
}
ret = append(ret, j...)
if i < len(feats)-1 {
ret = append(ret, byte('\n'))
}
}

return ret, nil
}

func makeHandlerFunc(cfg restConfig, handlerName string, f func(restConfig, http.ResponseWriter, *http.Request)) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if apiMeter := meters.ForContext(r.Context()); apiMeter != nil {
Expand Down
5 changes: 5 additions & 0 deletions server/rest/route_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func (r RouteRequest) ResponseKey() string { return "routes" }

// Query returns a GraphQL query string and variables.
func (r RouteRequest) Query() (string, map[string]interface{}) {
// These formats will need geometries included
if r.ID > 0 || r.Format == "geojson" || r.Format == "geojsonl" || r.Format == "png" {
r.IncludeGeometry = true
}

// Handle operator key
if r.AgencyKey == "" {
// pass
Expand Down
37 changes: 37 additions & 0 deletions server/rest/route_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rest

import (
"context"
"strings"
"testing"

"github.com/interline-io/transitland-server/model"
Expand Down Expand Up @@ -123,6 +124,42 @@ func TestRouteRequest(t *testing.T) {
}
}

func TestRouteRequest_Format(t *testing.T) {
tcs := []testRest{
{
name: "route geojson",
format: "geojson",
h: RouteRequest{FeedOnestopID: "CT", Format: "geojson", WithCursor: WithCursor{Limit: 5}},
f: func(t *testing.T, jj string) {
a := gjson.Get(jj, "features").Array()
assert.Equal(t, 5, len(a))
assert.Equal(t, "Feature", gjson.Get(jj, "features.0.type").String())
assert.Equal(t, "MultiLineString", gjson.Get(jj, "features.0.geometry.type").String())
assert.Equal(t, "CT", gjson.Get(jj, "features.0.properties.feed_version.feed.onestop_id").String())
assert.Greater(t, gjson.Get(jj, "meta.after").Int(), int64(0))
},
},
{
name: "route geojsonl",
format: "geojsonl",
h: RouteRequest{FeedOnestopID: "CT", Format: "geojsonl", WithCursor: WithCursor{Limit: 5}},
f: func(t *testing.T, jj string) {
split := strings.Split(jj, "\n")
assert.Equal(t, 5, len(split))
assert.Equal(t, "Feature", gjson.Get(split[0], "type").String())
assert.Equal(t, "MultiLineString", gjson.Get(split[0], "geometry.type").String())
assert.Equal(t, "CT", gjson.Get(split[0], "properties.feed_version.feed.onestop_id").String())
},
},
}
srv, te := testRestConfig(t)
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
testquery(t, srv, te, tc)
})
}
}

func TestRouteRequest_Pagination(t *testing.T) {
srv, te := testRestConfig(t)
allEnts, err := te.Finder.FindRoutes(context.Background(), nil, nil, nil, nil)
Expand Down
1 change: 1 addition & 0 deletions server/rest/stop_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type StopRequest struct {
Lon float64 `json:"lon,string"`
Lat float64 `json:"lat,string"`
Radius float64 `json:"radius,string"`
Format string `json:"format"`
ServedByOnestopIds string `json:"served_by_onestop_ids"`
ServedByRouteType *int `json:"served_by_route_type,string"`
IncludeAlerts bool `json:"include_alerts,string"`
Expand Down
37 changes: 37 additions & 0 deletions server/rest/stop_request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package rest

import (
"context"
"strings"
"testing"

"github.com/interline-io/transitland-server/model"
Expand Down Expand Up @@ -203,6 +204,42 @@ func TestStopRequest_AdminCache(t *testing.T) {
testquery(t, srv, te, tc)
}

func TestStopRequest_Format(t *testing.T) {
tcs := []testRest{
{
name: "stop geojson",
format: "geojson",
h: StopRequest{FeedOnestopID: "CT", Format: "geojson", WithCursor: WithCursor{Limit: 20}},
f: func(t *testing.T, jj string) {
a := gjson.Get(jj, "features").Array()
assert.Equal(t, 20, len(a))
assert.Equal(t, "Feature", gjson.Get(jj, "features.0.type").String())
assert.Equal(t, "Point", gjson.Get(jj, "features.0.geometry.type").String())
assert.Equal(t, "CT", gjson.Get(jj, "features.0.properties.feed_version.feed.onestop_id").String())
assert.Greater(t, gjson.Get(jj, "meta.after").Int(), int64(0))
},
},
{
name: "stop geojsonl",
format: "geojsonl",
h: StopRequest{FeedOnestopID: "CT", Format: "geojsonl", WithCursor: WithCursor{Limit: 20}},
f: func(t *testing.T, jj string) {
split := strings.Split(jj, "\n")
assert.Equal(t, 20, len(split))
assert.Equal(t, "Feature", gjson.Get(split[0], "type").String())
assert.Equal(t, "Point", gjson.Get(split[0], "geometry.type").String())
assert.Equal(t, "CT", gjson.Get(split[0], "properties.feed_version.feed.onestop_id").String())
},
},
}
srv, te := testRestConfig(t)
for _, tc := range tcs {
t.Run(tc.name, func(t *testing.T) {
testquery(t, srv, te, tc)
})
}
}

func TestStopRequest_Pagination(t *testing.T) {
srv, te := testRestConfig(t)
allEnts, err := te.Finder.FindStops(context.Background(), nil, nil, nil, nil)
Expand Down
20 changes: 9 additions & 11 deletions server/rest/trip_request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ type TripRequest struct {
RouteOnestopID string `json:"route_onestop_id"`
FeedOnestopID string `json:"feed_onestop_id"`
FeedVersionSHA1 string `json:"feed_version_sha1"`
IncludeGeometry string `json:"include_geometry"`
IncludeStopTimes string `json:"include_stop_times"`
ServiceDate string `json:"service_date"`
IncludeGeometry bool `json:"include_geometry,string"`
IncludeStopTimes bool `json:"include_stop_times,string"`
IncludeAlerts bool `json:"include_alerts,string"`
Format string
LicenseFilter
Expand Down Expand Up @@ -62,24 +62,22 @@ func (r TripRequest) Query() (string, map[string]interface{}) {
}
where["license"] = checkLicenseFilter(r.LicenseFilter)
// Include geometry when in geojson format
includeGeometry := false
if r.ID > 0 || r.IncludeGeometry == "true" || r.Format == "geojson" {
includeGeometry = true
if r.ID > 0 || r.Format == "geojson" || r.Format == "geojsonl" {
r.IncludeGeometry = true
}
// Only include stop times when requesting a specific trip.
includeStopTimes := false
// || r.IncludeStopTimes == "true"
if r.ID > 0 || r.Format == "geojson" {
includeStopTimes = true
r.IncludeStopTimes = false
if r.ID > 0 {
r.IncludeStopTimes = true
}
includeRoute := false
return tripQuery, hw{
"limit": r.CheckLimit(),
"after": r.CheckAfter(),
"ids": checkIds(r.ID),
"where": where,
"include_geometry": includeGeometry,
"include_stop_times": includeStopTimes,
"include_geometry": r.IncludeGeometry,
"include_stop_times": r.IncludeStopTimes,
"include_route": includeRoute,
"include_alerts": r.IncludeAlerts,
}
Expand Down
Loading