feat(media meta): reverse geocoding from mapbox (#2922)
parent
668b542c59
commit
5e5dca40c4
@ -1 +1 @@
|
|||||||
Subproject commit dece1c7098de2efe38aaa25d6cafc41a2de568ff
|
Subproject commit fc7791cde1444e1be0935f1fbc32d956fa6eb756
|
@ -0,0 +1,236 @@
|
|||||||
|
package mediameta
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/driver"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/filemanager/manager/entitysource"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/logging"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/request"
|
||||||
|
"github.com/cloudreve/Cloudreve/v4/pkg/setting"
|
||||||
|
)
|
||||||
|
|
||||||
|
const mapBoxURL = "https://api.mapbox.com/search/geocode/v6/reverse"
|
||||||
|
|
||||||
|
const (
|
||||||
|
Street = "street"
|
||||||
|
Locality = "locality"
|
||||||
|
Place = "place"
|
||||||
|
District = "district"
|
||||||
|
Region = "region"
|
||||||
|
Country = "country"
|
||||||
|
)
|
||||||
|
|
||||||
|
type geocodingExtractor struct {
|
||||||
|
settings setting.Provider
|
||||||
|
l logging.Logger
|
||||||
|
client request.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func newGeocodingExtractor(settings setting.Provider, l logging.Logger, client request.Client) *geocodingExtractor {
|
||||||
|
return &geocodingExtractor{
|
||||||
|
settings: settings,
|
||||||
|
l: l,
|
||||||
|
client: client,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *geocodingExtractor) Exts() []string {
|
||||||
|
return exifExts
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *geocodingExtractor) Extract(ctx context.Context, ext string, source entitysource.EntitySource, opts ...optionFunc) ([]driver.MediaMeta, error) {
|
||||||
|
option := &option{}
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.apply(option)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find GPS info from extracted
|
||||||
|
var latStr, lngStr string
|
||||||
|
for _, meta := range option.extracted {
|
||||||
|
if meta.Key == GpsLat {
|
||||||
|
latStr = meta.Value
|
||||||
|
}
|
||||||
|
if meta.Key == GpsLng {
|
||||||
|
lngStr = meta.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if latStr == "" || lngStr == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lat, err := strconv.ParseFloat(latStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("geocoding: failed to parse latitude: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lng, err := strconv.ParseFloat(lngStr, 64)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("geocoding: failed to parse longitude: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
metas, err := e.getGeocoding(ctx, lat, lng, option.language)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("geocoding: failed to get geocoding: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, _ := range metas {
|
||||||
|
metas[i].Type = driver.MetaTypeGeocoding
|
||||||
|
}
|
||||||
|
|
||||||
|
return metas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *geocodingExtractor) getGeocoding(ctx context.Context, lat, lng float64, language string) ([]driver.MediaMeta, error) {
|
||||||
|
values := url.Values{}
|
||||||
|
values.Add("longitude", fmt.Sprintf("%f", lng))
|
||||||
|
values.Add("latitude", fmt.Sprintf("%f", lat))
|
||||||
|
values.Add("limit", "1")
|
||||||
|
values.Add("access_token", e.settings.MediaMetaGeocodingMapboxAK(ctx))
|
||||||
|
if language != "" {
|
||||||
|
values.Add("language", language)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := e.client.Request(
|
||||||
|
"GET",
|
||||||
|
mapBoxURL+"?"+values.Encode(),
|
||||||
|
nil,
|
||||||
|
request.WithContext(ctx),
|
||||||
|
request.WithLogger(e.l),
|
||||||
|
).CheckHTTPResponse(http.StatusOK).GetResponse()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get geocoding from mapbox: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var geocoding MapboxGeocodingResponse
|
||||||
|
if err := json.Unmarshal([]byte(resp), &geocoding); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal geocoding from mapbox: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(geocoding.Features) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
metas := make([]driver.MediaMeta, 0)
|
||||||
|
contexts := geocoding.Features[0].Properties.Context
|
||||||
|
if contexts.Street != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: Street,
|
||||||
|
Value: contexts.Street.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if contexts.Locality != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: Locality,
|
||||||
|
Value: contexts.Locality.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if contexts.Place != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: Place,
|
||||||
|
Value: contexts.Place.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if contexts.District != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: District,
|
||||||
|
Value: contexts.District.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if contexts.Region != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: Region,
|
||||||
|
Value: contexts.Region.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if contexts.Country != nil {
|
||||||
|
metas = append(metas, driver.MediaMeta{
|
||||||
|
Key: Country,
|
||||||
|
Value: contexts.Country.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return metas, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MapboxGeocodingResponse represents the response from Mapbox Geocoding API
|
||||||
|
type MapboxGeocodingResponse struct {
|
||||||
|
Type string `json:"type"` // "FeatureCollection"
|
||||||
|
Features []Feature `json:"features"` // Array of feature objects
|
||||||
|
Attribution string `json:"attribution"` // Attribution to Mapbox
|
||||||
|
}
|
||||||
|
|
||||||
|
// Feature represents a feature object in the geocoding response
|
||||||
|
type Feature struct {
|
||||||
|
ID string `json:"id"` // Feature ID (same as mapbox_id)
|
||||||
|
Type string `json:"type"` // "Feature"
|
||||||
|
Geometry Geometry `json:"geometry"` // Spatial geometry of the feature
|
||||||
|
Properties Properties `json:"properties"` // Feature details
|
||||||
|
}
|
||||||
|
|
||||||
|
// Geometry represents the spatial geometry of a feature
|
||||||
|
type Geometry struct {
|
||||||
|
Type string `json:"type"` // "Point"
|
||||||
|
Coordinates []float64 `json:"coordinates"` // [longitude, latitude]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Properties contains the feature's detailed information
|
||||||
|
type Properties struct {
|
||||||
|
MapboxID string `json:"mapbox_id"` // Unique feature identifier
|
||||||
|
FeatureType string `json:"feature_type"` // Type of feature (country, region, etc.)
|
||||||
|
Name string `json:"name"` // Formatted address string
|
||||||
|
NamePreferred string `json:"name_preferred"` // Canonical or common alias
|
||||||
|
PlaceFormatted string `json:"place_formatted"` // Formatted context string
|
||||||
|
FullAddress string `json:"full_address"` // Full formatted address
|
||||||
|
Context Context `json:"context"` // Hierarchy of parent features
|
||||||
|
Coordinates Coordinates `json:"coordinates"` // Geographic position and accuracy
|
||||||
|
BBox []float64 `json:"bbox,omitempty"` // Bounding box [minLon,minLat,maxLon,maxLat]
|
||||||
|
MatchCode MatchCode `json:"match_code"` // Metadata about result matching
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context represents the hierarchy of encompassing parent features
|
||||||
|
type Context struct {
|
||||||
|
Country *ContextFeature `json:"country,omitempty"`
|
||||||
|
Region *ContextFeature `json:"region,omitempty"`
|
||||||
|
Postcode *ContextFeature `json:"postcode,omitempty"`
|
||||||
|
District *ContextFeature `json:"district,omitempty"`
|
||||||
|
Place *ContextFeature `json:"place,omitempty"`
|
||||||
|
Locality *ContextFeature `json:"locality,omitempty"`
|
||||||
|
Neighborhood *ContextFeature `json:"neighborhood,omitempty"`
|
||||||
|
Street *ContextFeature `json:"street,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ContextFeature represents a feature in the context hierarchy
|
||||||
|
type ContextFeature struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
NamePreferred string `json:"name_preferred,omitempty"`
|
||||||
|
MapboxID string `json:"mapbox_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Coordinates represents geographical position and accuracy information
|
||||||
|
type Coordinates struct {
|
||||||
|
Longitude float64 `json:"longitude"` // Longitude of result
|
||||||
|
Latitude float64 `json:"latitude"` // Latitude of result
|
||||||
|
Accuracy string `json:"accuracy,omitempty"` // Accuracy metric for address results
|
||||||
|
RoutablePoints []RoutablePoint `json:"routable_points,omitempty"` // Array of routable points
|
||||||
|
}
|
||||||
|
|
||||||
|
// RoutablePoint represents a routable point for an address feature
|
||||||
|
type RoutablePoint struct {
|
||||||
|
Name string `json:"name"` // Name of the routable point
|
||||||
|
Longitude float64 `json:"longitude"` // Longitude coordinate
|
||||||
|
Latitude float64 `json:"latitude"` // Latitude coordinate
|
||||||
|
}
|
||||||
|
|
||||||
|
// MatchCode contains metadata about how result components match the input query
|
||||||
|
type MatchCode struct {
|
||||||
|
// Add specific match code fields as needed based on Mapbox documentation
|
||||||
|
// This structure may vary depending on the specific match codes returned
|
||||||
|
}
|
Loading…
Reference in new issue