go-places/api/internal/domain/models.go
2026-10-07 15:14:50 +02:00

166 lines
7 KiB
Go

// Package domain contains the core entities of the application. It has no
// dependencies on transport, storage or database layers so that the business
// rules can be reasoned about in isolation.
package domain
import "time"
// User represents a logged-in user. Identity is owned by the external go-loose
// SSO service; this service only stores the identifier it asserts.
type User struct {
ID string `json:"id" gql:"id"`
Username string `json:"username" gql:"username"`
Email string `json:"email" gql:"email"`
}
// Coordinate is a WGS-84 geographic point.
type Coordinate struct {
Latitude float64 `json:"latitude" gql:"latitude"`
Longitude float64 `json:"longitude" gql:"longitude"`
}
// Valid reports whether the coordinate is a real point on Earth.
func (c Coordinate) Valid() bool {
if c.Latitude < -90 || c.Latitude > 90 {
return false
}
return c.Longitude >= -180 && c.Longitude <= 180
}
// AnalysisStatus tracks the progress of the image analysis pipeline. Analysis
// runs out of band, so a picture is visible to the API before its badges exist.
type AnalysisStatus string
const (
AnalysisPending AnalysisStatus = "pending"
AnalysisCompleted AnalysisStatus = "completed"
AnalysisFailed AnalysisStatus = "failed"
)
// Badge is a recognisable attribute detected in a picture, for example an
// object ("dog"), a scene ("beach") or a property ("low-light").
type Badge struct {
// Label is the stable machine-readable name, e.g. "dog" or "beach".
Label string `json:"label" gql:"label"`
// Display is a human-friendly rendering of the label, e.g. "Dog".
Display string `json:"display" gql:"display"`
// Confidence is a 0..1 score. Heuristic badges tend to be lower confidence
// than those returned by a trained model.
Confidence float64 `json:"confidence" gql:"confidence"`
// Source records which analyzer produced the badge, which lets the UI
// distinguish machine-derived facts from heuristics.
Source string `json:"source" gql:"source"`
}
// Image is a picture attached to a Place.
type Image struct {
ID string `json:"id" gql:"id"`
PlaceID string `json:"placeId" gql:"placeId"`
// StorageKey is the location of the bytes inside the blob store. It is not
// exposed to clients; URL is the public rendering of this key.
StorageKey string `json:"-" gql:"-"`
URL string `json:"url" gql:"url"`
Caption string `json:"caption" gql:"caption"`
ContentType string `json:"contentType" gql:"contentType"`
ByteSize int64 `json:"byteSize" gql:"byteSize"`
Width int `json:"width" gql:"width"`
Height int `json:"height" gql:"height"`
// Coordinate is where the picture was taken. It is taken from the EXIF GPS
// tag when present, and falls back to the parent Place otherwise.
Coordinate *Coordinate `json:"coordinate" gql:"coordinate"`
// TakenAt is when the picture was captured, from the EXIF timestamp.
TakenAt *time.Time `json:"takenAt" gql:"takenAt"`
// CameraMake and CameraModel come from EXIF and identify the capture device.
CameraMake string `json:"cameraMake" gql:"cameraMake"`
CameraModel string `json:"cameraModel" gql:"cameraModel"`
Badges []Badge `json:"badges" gql:"badges"`
AnalysisStatus AnalysisStatus `json:"analysisStatus" gql:"analysisStatus"`
AnalysisError string `json:"analysisError" gql:"analysisError"`
// LinkedImages are other pictures taken at the same location, resolved by
// the linking pass rather than stored.
LinkedImages []Image `json:"linkedImages" gql:"linkedImages"`
CreatedAt time.Time `json:"createdAt" gql:"createdAt"`
}
// Place is a location the user visited and shared. Pictures and a free-text
// description hang off it, and it is the anchor for geographic linking.
type Place struct {
ID string `json:"id" gql:"id"`
Title string `json:"title" gql:"title"`
Description string `json:"description" gql:"description"`
// Coordinate is the location of the place. Required for linking to work;
// when a picture carries its own EXIF GPS tag that value takes precedence.
Coordinate *Coordinate `json:"coordinate" gql:"coordinate"`
// LinkRadiusMeters is how close two pictures must be to count as having
// been taken at the same place.
LinkRadiusMeters float64 `json:"linkRadiusMeters" gql:"linkRadiusMeters"`
Creator User `json:"creator" gql:"creator"`
Images []Image `json:"images" gql:"images"`
Comments []Comment `json:"comments" gql:"comments"`
// RelatedPlaces are other Places whose pictures overlap this one.
RelatedPlaces []RelatedPlace `json:"relatedPlaces" gql:"relatedPlaces"`
CreatedAt time.Time `json:"createdAt" gql:"createdAt"`
}
// RelatedPlace records the link between two Places discovered by the linking
// pass, along with the evidence for it.
type RelatedPlace struct {
Place Place `json:"place" gql:"place"`
DistanceMeters float64 `json:"distanceMeters" gql:"distanceMeters"`
SharedImageIDs []string `json:"sharedImageIds" gql:"sharedImageIds"`
}
// Link is a single edge produced by the linking pass, connecting one picture to
// another that was taken at the same place.
type Link struct {
ImageID string `json:"imageId" gql:"imageId"`
RelatedImageID string `json:"relatedImageId" gql:"relatedImageId"`
// DistanceMeters is the great-circle distance between the two capture points.
DistanceMeters float64 `json:"distanceMeters" gql:"distanceMeters"`
// SamePlace is true when the two pictures belong to the same Place record.
SamePlace bool `json:"samePlace" gql:"samePlace"`
// Reason explains the evidence, e.g. "same-place" or "within-radius".
Reason string `json:"reason" gql:"reason"`
}
// Comment is a user reaction on a Place.
type Comment struct {
ID string `json:"id" gql:"id"`
PlaceID string `json:"placeId" gql:"placeId"`
Content string `json:"content" gql:"content"`
Creator User `json:"creator" gql:"creator"`
CreatedAt time.Time `json:"createdAt" gql:"createdAt"`
}
// PlaceInput is the payload for creating a Place. Latitude and Longitude are
// pointers so that an omitted coordinate can be told apart from the origin,
// which is a legitimate coordinate.
type PlaceInput struct {
Title string `json:"title" gql:"title"`
Description string `json:"description" gql:"description"`
Latitude *float64 `json:"latitude" gql:"latitude"`
Longitude *float64 `json:"longitude" gql:"longitude"`
LinkRadiusMeters *float64 `json:"linkRadiusMeters" gql:"linkRadiusMeters"`
// Caption is the description of the first picture in the upload, if any.
Caption string `json:"caption" gql:"caption"`
}
// Coordinate builds a Coordinate from the optional latitude and longitude,
// returning nil when they are absent or do not form a valid point.
func (p PlaceInput) Coordinate() *Coordinate {
if p.Latitude == nil || p.Longitude == nil {
return nil
}
c := Coordinate{Latitude: *p.Latitude, Longitude: *p.Longitude}
if !c.Valid() {
return nil
}
return &c
}