You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
cloudreve/pkg/mediameta/geocoding.go

237 lines
7.3 KiB

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
}