feat: cache geojson data for vector files
This commit is contained in:
@@ -34,6 +34,7 @@ type DatasetRepository interface {
|
||||
MarkReady(ctx context.Context, id uuid.UUID, geometry []byte) error
|
||||
MarkConverted(ctx context.Context, id uuid.UUID, cogKey string, footprint []byte) error
|
||||
SetProperties(ctx context.Context, id uuid.UUID, properties, geometry []byte) error
|
||||
SetGeoJSON(ctx context.Context, id uuid.UUID, geojson []byte) error
|
||||
SaveMapping(ctx context.Context, id uuid.UUID, katoColumn string, years []domain.YearColumn) (domain.Dataset, error)
|
||||
ReplaceObservations(ctx context.Context, datasetID uuid.UUID, obs []domain.Observation) error
|
||||
ListObservations(ctx context.Context, datasetID uuid.UUID, katoCode *string, limit, offset int) ([]domain.Observation, error)
|
||||
@@ -317,7 +318,12 @@ func (s *DatasetService) ExtractProperties(ctx context.Context, id uuid.UUID) er
|
||||
}
|
||||
|
||||
geometry := s.vectorGeometry(ctx, dataset.Filename, data)
|
||||
return s.repo.SetProperties(ctx, id, properties, geometry)
|
||||
if err := s.repo.SetProperties(ctx, id, properties, geometry); err != nil {
|
||||
return err
|
||||
}
|
||||
// Assemble and persist the GeoJSON served by the .geojson endpoint from the
|
||||
// now-canonical (unioned) geometry and attribute table.
|
||||
return s.generateGeoJSON(ctx, id)
|
||||
}
|
||||
|
||||
// vectorGeometry dissolves a vector file's features into a single GeoJSON
|
||||
@@ -558,7 +564,12 @@ func (s *DatasetService) Extract(ctx context.Context, id uuid.UUID) error {
|
||||
}
|
||||
|
||||
geometry := s.vectorGeometry(ctx, dataset.Filename, data)
|
||||
return s.repo.MarkReady(ctx, id, geometry)
|
||||
if err := s.repo.MarkReady(ctx, id, geometry); err != nil {
|
||||
return err
|
||||
}
|
||||
// Assemble and persist the GeoJSON served by the .geojson endpoint by joining
|
||||
// the districts table on the freshly extracted observations.
|
||||
return s.generateGeoJSON(ctx, id)
|
||||
}
|
||||
|
||||
// buildObservations unpivots rows into observations. Rows without a KATO code
|
||||
@@ -629,33 +640,82 @@ func (s *DatasetService) ListObservations(ctx context.Context, id uuid.UUID, kat
|
||||
return ObservationPage{Items: items, Page: page, PageSize: pageSize, Total: total}, nil
|
||||
}
|
||||
|
||||
// GeoJSON assembles a GeoJSON FeatureCollection (RFC 7946) for a vector or
|
||||
// vector_with_kato dataset.
|
||||
// emptyFeatureCollectionJSON is served when a dataset has no persisted GeoJSON
|
||||
// (e.g. it carried no geometry), so the endpoint always returns a valid RFC 7946
|
||||
// FeatureCollection.
|
||||
var emptyFeatureCollectionJSON = json.RawMessage(`{"type":"FeatureCollection","features":[]}`)
|
||||
|
||||
// GeoJSON returns the pre-assembled GeoJSON FeatureCollection (RFC 7946) stored
|
||||
// for a vector or vector_with_kato dataset. The collection is normally generated
|
||||
// and persisted at processing time (see generateGeoJSON), and this method serves
|
||||
// the stored column verbatim.
|
||||
//
|
||||
// A plain vector dataset has no KATO mapping or observations, so the result is a
|
||||
// single geometry-only Feature wrapping the dataset's own (dissolved) geometry,
|
||||
// with empty properties (or an empty collection when the dataset has no
|
||||
// geometry).
|
||||
// For backward compatibility with datasets that became ready before the column
|
||||
// existed, an empty column is assembled on demand, persisted (so subsequent
|
||||
// requests are served from the cached column), and returned.
|
||||
//
|
||||
// Only ready datasets are served; a dataset still being processed yields a
|
||||
// conflict, and unsupported file types (e.g. raster) yield a validation error.
|
||||
func (s *DatasetService) GeoJSON(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
|
||||
dataset, err := s.loadGeoJSONDataset(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(dataset.GeoJSON) > 0 {
|
||||
return dataset.GeoJSON, nil
|
||||
}
|
||||
|
||||
// Empty column (ready dataset processed before GeoJSON was persisted at
|
||||
// processing time): assemble it now, cache it, and return it.
|
||||
raw, err := s.buildGeoJSON(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(raw) == 0 {
|
||||
raw = emptyFeatureCollectionJSON
|
||||
}
|
||||
if err := s.repo.SetGeoJSON(ctx, id, raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// generateGeoJSON assembles the dataset's GeoJSON FeatureCollection and persists
|
||||
// it. It is invoked at processing time for vector and vector_with_kato datasets;
|
||||
// rasters (and any other type) store nothing.
|
||||
func (s *DatasetService) generateGeoJSON(ctx context.Context, id uuid.UUID) error {
|
||||
raw, err := s.buildGeoJSON(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return s.repo.SetGeoJSON(ctx, id, raw)
|
||||
}
|
||||
|
||||
// buildGeoJSON assembles a GeoJSON FeatureCollection (RFC 7946) for a dataset
|
||||
// from its persisted state, returning the marshaled bytes (nil for unsupported
|
||||
// file types).
|
||||
//
|
||||
// A plain vector dataset yields a single geometry-only Feature wrapping the
|
||||
// dataset's own (dissolved) geometry, exposing its extracted attribute table as
|
||||
// the Feature's properties (or an empty collection when it has no geometry).
|
||||
//
|
||||
// A vector_with_kato dataset always ignores any geometry it carries and instead
|
||||
// joins the districts table on KATO code: one Feature is emitted per KATO, its
|
||||
// boundary taken from the districts table and its observation values nested
|
||||
// under a `data` object (keyed by date) alongside `kato` and `name`. KATO codes
|
||||
// with no matching district are skipped.
|
||||
//
|
||||
// Only ready datasets are served; a dataset still being processed yields a
|
||||
// conflict.
|
||||
func (s *DatasetService) GeoJSON(ctx context.Context, id uuid.UUID) (domain.FeatureCollection, error) {
|
||||
dataset, err := s.loadGeoJSONDataset(ctx, id)
|
||||
func (s *DatasetService) buildGeoJSON(ctx context.Context, id uuid.UUID) (json.RawMessage, error) {
|
||||
dataset, err := s.repo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return domain.FeatureCollection{}, err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Plain vector: no KATO mapping or observations. Return the dataset's own
|
||||
// geometry as a single Feature, exposing the extracted attribute table (e.g.
|
||||
// a GeoPackage's table data) as the Feature's top-level properties.
|
||||
if dataset.FileType == domain.FileTypeVector {
|
||||
fc := domain.FeatureCollection{Type: domain.GeoJSONFeatureCollection, Features: []domain.Feature{}}
|
||||
var fc domain.FeatureCollection
|
||||
switch dataset.FileType {
|
||||
case domain.FileTypeVector:
|
||||
// Plain vector: the dataset's own geometry as a single Feature, exposing the
|
||||
// extracted attribute table (e.g. a GeoPackage's table data) as properties.
|
||||
fc = domain.FeatureCollection{Type: domain.GeoJSONFeatureCollection, Features: []domain.Feature{}}
|
||||
if hasGeometry(dataset.Geometry) {
|
||||
fc.Features = append(fc.Features, domain.Feature{
|
||||
Type: domain.GeoJSONFeature,
|
||||
@@ -663,21 +723,24 @@ func (s *DatasetService) GeoJSON(ctx context.Context, id uuid.UUID) (domain.Feat
|
||||
Properties: vectorFeatureProperties(dataset.Properties),
|
||||
})
|
||||
}
|
||||
return fc, nil
|
||||
case domain.FileTypeVectorWithKato:
|
||||
// Always ignore the dataset's own geometry; build one Feature per KATO from
|
||||
// the districts table.
|
||||
obs, err := s.repo.ListAllObservations(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
grouped, order := groupObservationsByKato(obs)
|
||||
features, err := s.districtFeatures(ctx, grouped, order)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fc = domain.FeatureCollection{Type: domain.GeoJSONFeatureCollection, Features: features}
|
||||
default:
|
||||
return nil, nil // rasters carry no GeoJSON
|
||||
}
|
||||
|
||||
// vector_with_kato: always ignore the dataset's own geometry and build one
|
||||
// Feature per KATO from the districts table.
|
||||
obs, err := s.repo.ListAllObservations(ctx, id)
|
||||
if err != nil {
|
||||
return domain.FeatureCollection{}, err
|
||||
}
|
||||
grouped, order := groupObservationsByKato(obs)
|
||||
features, err := s.districtFeatures(ctx, grouped, order)
|
||||
if err != nil {
|
||||
return domain.FeatureCollection{}, err
|
||||
}
|
||||
return domain.FeatureCollection{Type: domain.GeoJSONFeatureCollection, Features: features}, nil
|
||||
return json.Marshal(fc)
|
||||
}
|
||||
|
||||
// loadGeoJSONDataset fetches a dataset for the GeoJSON endpoint and validates
|
||||
|
||||
Reference in New Issue
Block a user