166 lines
7 KiB
Go
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
|
|
}
|