From ca8cfffc24f9f7d839400d6ba04b63c1cf466e83 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sat, 28 Dec 2019 20:24:20 +0100 Subject: [PATCH] Backend: Prepare database for advanced filtering and grouping #154 Signed-off-by: Michael Mayer --- frontend/src/common/config.js | 3 + frontend/src/component/p-photo-list.vue | 2 +- frontend/src/model/photo.js | 4 +- frontend/tests/unit/model/photo_test.js | 4 +- internal/colors/colors.go | 462 +++++++++++++++++++++++ internal/colors/colors_test.go | 11 + internal/config/config.go | 46 ++- internal/entity/country.go | 15 + internal/entity/location.go | 81 ++-- internal/entity/photo.go | 26 +- internal/entity/place.go | 108 ++++++ internal/maps/countries.go | 3 +- internal/maps/countries.json | 4 + internal/maps/location.go | 28 +- internal/maps/location_test.go | 30 +- internal/maps/osm/location.go | 4 + internal/maps/osm/location_test.go | 7 +- internal/photoprism/colors.go | 414 +------------------- internal/photoprism/colors_test.go | 43 +-- internal/photoprism/indexer_mediafile.go | 44 ++- internal/photoprism/location_test.go | 4 +- internal/repo/category.go | 33 ++ internal/repo/category_test.go | 17 + internal/repo/photo.go | 22 +- internal/util/date.go | 17 + internal/util/date_test.go | 13 + 26 files changed, 927 insertions(+), 518 deletions(-) create mode 100644 internal/colors/colors.go create mode 100644 internal/colors/colors_test.go create mode 100644 internal/entity/place.go create mode 100644 internal/repo/category.go create mode 100644 internal/repo/category_test.go create mode 100644 internal/util/date.go create mode 100644 internal/util/date_test.go diff --git a/frontend/src/common/config.js b/frontend/src/common/config.js index 9213ed1ef..d7d321ccf 100644 --- a/frontend/src/common/config.js +++ b/frontend/src/common/config.js @@ -60,6 +60,9 @@ class Config { case "countries": this.values.count.countries += data.count; break; + case "places": + this.values.count.places += data.count; + break; case "labels": this.values.count.labels += data.count; break; diff --git a/frontend/src/component/p-photo-list.vue b/frontend/src/component/p-photo-list.vue index 9a16dfa2c..afa21cc72 100644 --- a/frontend/src/component/p-photo-list.vue +++ b/frontend/src/component/p-photo-list.vue @@ -49,7 +49,7 @@ {text: '', value: '', align: 'center', sortable: false, class: 'p-col-select'}, {text: this.$gettext('Title'), value: 'PhotoTitle'}, {text: this.$gettext('Taken At'), value: 'TakenAt'}, - {text: this.$gettext('Location'), value: 'LocPlace'}, + {text: this.$gettext('Location'), value: 'LocLabel'}, {text: this.$gettext('Camera'), value: 'CameraModel'}, {text: this.$gettext('Favorite'), value: 'PhotoFavorite', align: 'left'}, ], diff --git a/frontend/src/model/photo.js b/frontend/src/model/photo.js index d6f785c2a..c7c38966e 100644 --- a/frontend/src/model/photo.js +++ b/frontend/src/model/photo.js @@ -104,8 +104,8 @@ class Photo extends Abstract { } getLocation() { - if (this.LocPlace) { - return this.LocPlace; + if (this.LocLabel) { + return this.LocLabel; } return "Unknown"; diff --git a/frontend/tests/unit/model/photo_test.js b/frontend/tests/unit/model/photo_test.js index cbcc1b001..249e157c4 100644 --- a/frontend/tests/unit/model/photo_test.js +++ b/frontend/tests/unit/model/photo_test.js @@ -135,14 +135,14 @@ describe("model/photo", () => { }); it("should get location", () => { - const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocPlace: "Cape Point, South Africa", LocCountry: "South Africa"}; + const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, South Africa", LocCountry: "South Africa"}; const photo = new Photo(values); const result = photo.getLocation(); assert.equal(result, "Cape Point, South Africa"); }); it("should get location", () => { - const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocPlace: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"}; + const values = {ID: 5, PhotoTitle: "Crazy Cat", LocationID: 6, LocType: "viewpoint", LocLabel: "Cape Point, State, South Africa", LocCountry: "South Africa", LocCity: "Cape Town", LocCounty: "County", LocState: "State"}; const photo = new Photo(values); const result = photo.getLocation(); assert.equal(result, "Cape Point, State, South Africa"); diff --git a/internal/colors/colors.go b/internal/colors/colors.go new file mode 100644 index 000000000..f592bbd30 --- /dev/null +++ b/internal/colors/colors.go @@ -0,0 +1,462 @@ +/* +This package encapsulates session storage. + +Additional information can be found in our Developer Guide: + +https://github.com/photoprism/photoprism/wiki +*/ +package colors + +import ( + "fmt" + "image/color" + "strings" + + "github.com/lucasb-eyer/go-colorful" +) + +type ColorPerception struct { + Colors Colors + MainColor Color + Luminance LightMap + Chroma Chroma +} + +type Color uint16 +type Colors []Color + +type Chroma uint8 +type Luminance uint8 +type LightMap []Luminance + +const ( + Black Color = iota + Brown + Grey + White + Purple + Gold + Blue + Cyan + Teal + Green + Lime + Yellow + Magenta + Orange + Red + Pink +) + +var All = Colors{ + Red, + Magenta, + Pink, + Orange, + Gold, + Yellow, + Lime, + Green, + Teal, + Cyan, + Blue, + Purple, + Brown, + White, + Grey, + Black, +} + +var Names = map[Color]string{ + Black: "dark", // 0 + Brown: "brown", // 1 + Grey: "grey", // 2 + White: "bright", // 3 + Purple: "purple", // 4 + Gold: "gold", // 5 + Blue: "blue", // 6 + Cyan: "cyan", // 7 + Teal: "teal", // 8 + Green: "green", // 9 + Lime: "lime", // A + Yellow: "yellow", // B + Magenta: "magenta", // C + Orange: "orange", // D + Red: "red", // E + Pink: "pink", // F +} + +var Weights = map[Color]uint16{ + Black: 2, + Brown: 2, + Grey: 1, + White: 2, + Purple: 4, + Gold: 4, + Blue: 3, + Cyan: 4, + Teal: 4, + Green: 3, + Lime: 5, + Yellow: 5, + Magenta: 5, + Orange: 4, + Red: 4, + Pink: 4, +} + +func (c Color) Name() string { + return Names[c] +} + +func (c Color) Hex() string { + return fmt.Sprintf("%X", c) +} + +func (c Colors) Hex() (result string) { + for _, indexedColor := range c { + result += indexedColor.Hex() + } + + return result +} + +func (c Colors) List() []map[string]string { + result := make([]map[string]string, 0, len(c)) + + for _, c := range c { + result = append(result, map[string]string{"name": c.Name(), "label": strings.Title(c.Name()), "example": ColorExamples[c]}) + } + + return result +} + +func (c Chroma) Hex() string { + return fmt.Sprintf("%X", c) +} + +func (c Chroma) Uint() uint { + return uint(c) +} + +func (c Chroma) Int() int { + return int(c) +} + +func (l Luminance) Hex() string { + return fmt.Sprintf("%X", l) +} + +func (m LightMap) Hex() (result string) { + for _, luminance := range m { + result += luminance.Hex() + } + + return result +} + +var ColorExamples = map[Color]string{ + Red: "#E57373", + Magenta: "#FF00FF", + Pink: "#F06292", + Orange: "#FFB74D", + Brown: "#A1887F", + Gold: "#FFD54F", + Yellow: "#FFF176", + Lime: "#DCE775", + Green: "#81C784", + Teal: "#4DB6AC", + Cyan: "#4DD0E1", + Blue: "#64B5F6", + Purple: "#BA68C8", + White: "#F5F5F5", + Grey: "#BDBDBD", + Black: "#333333", +} + +var ColorMap = map[color.RGBA]Color{ + {0x00, 0x00, 0x00, 0xff}: Black, + {0xa1, 0x88, 0x7f, 0xff}: Brown, + {0x8d, 0x6e, 0x63, 0xff}: Brown, + {0xa0, 0x7f, 0x6c, 0xff}: Brown, + {0x9b, 0x7b, 0x5b, 0xff}: Brown, + {0x75, 0x64, 0x5b, 0xff}: Brown, + {0x79, 0x55, 0x48, 0xff}: Brown, + {0x6d, 0x4c, 0x41, 0xff}: Brown, + {0x5d, 0x40, 0x37, 0xff}: Brown, + {0x9b, 0x61, 0x36, 0xff}: Brown, + {0xc1, 0xa4, 0x87, 0xff}: Brown, + {0xaa, 0x80, 0x62, 0xff}: Brown, + {0x6b, 0x55, 0x46, 0xff}: Brown, + {0xb4, 0xb5, 0x9c, 0xff}: Brown, + {0xb2, 0xb4, 0x9b, 0xff}: Green, + {0xe0, 0xe0, 0xe0, 0xff}: Grey, + {0x9E, 0x9E, 0x9E, 0xff}: Grey, + {0x75, 0x75, 0x75, 0xff}: Grey, + {0x61, 0x61, 0x61, 0xff}: Grey, + {0x42, 0x42, 0x42, 0xff}: Grey, + {0x84, 0x7a, 0x72, 0xff}: Grey, + {0xdf, 0xe0, 0xe1, 0xff}: Grey, + {0xFF, 0xFF, 0xFF, 0xff}: White, + {0xe4, 0xe4, 0xe4, 0xff}: White, + {0xe7, 0xe7, 0xe7, 0xff}: White, + {0xf3, 0xe5, 0xf5, 0xff}: Purple, + {0xe1, 0xbe, 0xe7, 0xff}: Purple, + {0xce, 0x93, 0xd8, 0xff}: Purple, + {0xba, 0x68, 0xc8, 0xff}: Purple, + {0xab, 0x47, 0xbc, 0xff}: Purple, + {0x9c, 0x27, 0xb0, 0xff}: Purple, + {0x9b, 0x31, 0x8f, 0xff}: Purple, + {0x86, 0x00, 0x7e, 0xff}: Purple, + {0x8e, 0x24, 0xaa, 0xff}: Purple, + {0x7b, 0x1f, 0xa2, 0xff}: Purple, + {0x6a, 0x1b, 0x9a, 0xff}: Purple, + {0x4a, 0x14, 0x8c, 0xff}: Purple, + {0xaa, 0x00, 0xff, 0xff}: Purple, + {0xed, 0xe7, 0xf6, 0xff}: Purple, + {0xd1, 0xc4, 0xe9, 0xff}: Purple, + {0xb3, 0x9d, 0xdb, 0xff}: Purple, + {0x95, 0x75, 0xcd, 0xff}: Purple, + {0x7e, 0x57, 0xc2, 0xff}: Purple, + {0x5e, 0x35, 0xb1, 0xff}: Purple, + {0x67, 0x3a, 0xb7, 0xff}: Purple, + {0x51, 0x2d, 0xa8, 0xff}: Purple, + {0x45, 0x27, 0xa0, 0xff}: Purple, + {0x31, 0x1b, 0x92, 0xff}: Purple, + {0xb3, 0x88, 0xff, 0xff}: Purple, + {0x7c, 0x4d, 0xff, 0xff}: Purple, + {0x8e, 0x64, 0x93, 0xff}: Purple, + {0x5e, 0x3a, 0x5e, 0xff}: Purple, + {0x44, 0x0e, 0x79, 0xff}: Purple, + {0x48, 0x36, 0x78, 0xff}: Purple, + {0x4e, 0x38, 0x80, 0xff}: Purple, + {0x3b, 0x0e, 0x79, 0xff}: Purple, + {0x3F, 0x51, 0xB5, 0xff}: Blue, + {0xc5, 0xca, 0xe9, 0xff}: Blue, + {0x5c, 0x6b, 0xc0, 0xff}: Blue, + {0x39, 0x49, 0xab, 0xff}: Blue, + {0x30, 0x3f, 0x9f, 0xff}: Blue, + {0x28, 0x35, 0x93, 0xff}: Blue, + {0x1a, 0x23, 0x7e, 0xff}: Blue, + {0x53, 0x6d, 0xfe, 0xff}: Blue, + {0x3d, 0x5a, 0xfe, 0xff}: Blue, + {0x30, 0x4f, 0xfe, 0xff}: Blue, + {0x21, 0x96, 0xF3, 0xff}: Blue, + {0xbb, 0xde, 0xfb, 0xff}: Blue, + {0x90, 0xca, 0xf9, 0xff}: Blue, + {0x64, 0xb5, 0xf6, 0xff}: Blue, + {0x42, 0xa5, 0xf5, 0xff}: Blue, + {0x1e, 0x88, 0xe5, 0xff}: Blue, + {0x19, 0x76, 0xd2, 0xff}: Blue, + {0x15, 0x65, 0xc0, 0xff}: Blue, + {0x0d, 0x47, 0xa1, 0xff}: Blue, + {0x82, 0xb1, 0xff, 0xff}: Blue, + {0x44, 0x8a, 0xff, 0xff}: Blue, + {0x29, 0x79, 0xff, 0xff}: Blue, + {0x29, 0x62, 0xff, 0xff}: Blue, + {0x03, 0xa9, 0xf6, 0xff}: Blue, + {0xb3, 0xe5, 0xfc, 0xff}: Blue, + {0x81, 0xd4, 0xfa, 0xff}: Blue, + {0x4f, 0xc3, 0xf7, 0xff}: Blue, + {0x29, 0xb6, 0xf6, 0xff}: Blue, + {0x03, 0x9b, 0xe5, 0xff}: Blue, + {0x02, 0x88, 0xd1, 0xff}: Blue, + {0x02, 0x77, 0xbd, 0xff}: Blue, + {0x01, 0x57, 0x9b, 0xff}: Blue, + {0x80, 0xd8, 0xff, 0xff}: Blue, + {0x40, 0xc4, 0xff, 0xff}: Blue, + {0x00, 0xb0, 0xff, 0xff}: Blue, + {0x00, 0x91, 0xea, 0xff}: Blue, + {0x60, 0x7d, 0x8b, 0xff}: Blue, + {0x78, 0x90, 0x9c, 0xff}: Blue, + {0x54, 0x6e, 0x7a, 0xff}: Blue, + {0x37, 0x47, 0x4f, 0xff}: Blue, + {0xe4, 0xeb, 0xfd, 0xff}: Blue, + {0x7d, 0xd3, 0xea, 0xff}: Blue, + {0x07, 0x63, 0x99, 0xff}: Blue, + {0x28, 0x44, 0x6b, 0xff}: Blue, + {0x4a, 0xc8, 0xf5, 0xff}: Blue, + {0x08, 0x00, 0xf4, 0xff}: Blue, + {0x01, 0x2d, 0x5f, 0xff}: Blue, + {0xb2, 0xeb, 0xf2, 0xff}: Cyan, + {0x80, 0xde, 0xea, 0xff}: Cyan, + {0x4d, 0xd0, 0xe1, 0xff}: Cyan, + {0x26, 0xc6, 0xda, 0xff}: Cyan, + {0x00, 0xb8, 0xd4, 0xff}: Cyan, + {0x00, 0xBC, 0xD4, 0xff}: Cyan, + {0x00, 0xac, 0xc1, 0xff}: Cyan, + {0x00, 0x97, 0xa7, 0xff}: Cyan, + {0x00, 0x83, 0x8f, 0xff}: Cyan, + {0x00, 0x60, 0x64, 0xff}: Cyan, + {0x84, 0xff, 0xff, 0xff}: Cyan, + {0x18, 0xff, 0xff, 0xff}: Cyan, + {0x00, 0xe5, 0xff, 0xff}: Cyan, + {0x00, 0x96, 0x88, 0xff}: Teal, + {0x00, 0x89, 0x7b, 0xff}: Teal, + {0x00, 0x79, 0x6b, 0xff}: Teal, + {0x00, 0x69, 0x5c, 0xff}: Teal, + {0x04, 0x5d, 0x5c, 0xff}: Teal, + {0x24, 0x5a, 0x5f, 0xff}: Teal, + {0x03, 0x45, 0x4f, 0xff}: Teal, + {0x2c, 0x54, 0x5e, 0xff}: Teal, + {0x17, 0x47, 0x41, 0xff}: Teal, + {0xe8, 0xf5, 0xe9, 0xff}: Green, + {0xc8, 0xe6, 0xc9, 0xff}: Green, + {0xab, 0xc7, 0xb0, 0xff}: Green, + {0xa5, 0xd6, 0xa7, 0xff}: Green, + {0x81, 0xc7, 0x84, 0xff}: Green, + {0x66, 0xbb, 0x6a, 0xff}: Green, + {0x4C, 0xAF, 0x50, 0xff}: Green, + {0x43, 0xa0, 0x47, 0xff}: Green, + {0x38, 0x8e, 0x3c, 0xff}: Green, + {0x2e, 0x7d, 0x32, 0xff}: Green, + {0x1b, 0x5e, 0x20, 0xff}: Green, + {0xf1, 0xf8, 0xe9, 0xff}: Green, + {0xdc, 0xed, 0xc8, 0xff}: Green, + {0xc5, 0xe1, 0xa5, 0xff}: Green, + {0xae, 0xd5, 0x81, 0xff}: Green, + {0x8b, 0xc3, 0x4a, 0xff}: Green, + {0x9c, 0xcc, 0x65, 0xff}: Green, + {0x7c, 0xb3, 0x42, 0xff}: Green, + {0x68, 0x9f, 0x38, 0xff}: Green, + {0x55, 0x8b, 0x2f, 0xff}: Green, + {0x33, 0x69, 0x1e, 0xff}: Green, + {0xb9, 0xf6, 0xca, 0xff}: Green, + {0x69, 0xf0, 0xae, 0xff}: Green, + {0x00, 0xc8, 0x53, 0xff}: Green, + {0x00, 0xe6, 0x76, 0xff}: Green, + {0xcc, 0xff, 0x90, 0xff}: Green, + {0xb2, 0xff, 0x59, 0xff}: Green, + {0x76, 0xff, 0x03, 0xff}: Green, + {0x64, 0xdd, 0x17, 0xff}: Green, + {0xdd, 0xd5, 0x79, 0xff}: Green, + {0xee, 0xec, 0xa2, 0xff}: Green, + {0x24, 0x4e, 0x3b, 0xff}: Green, + {0x9a, 0x9d, 0x47, 0xff}: Green, + {0xbe, 0xbd, 0x76, 0xff}: Green, + {0x5c, 0x5a, 0x30, 0xff}: Green, + {0xb3, 0xc1, 0x6c, 0xff}: Green, + {0xac, 0xa7, 0x83, 0xff}: Green, + {0x47, 0x4c, 0x25, 0xff}: Green, + {0xcd, 0xd0, 0x87, 0xff}: Green, + {0x79, 0x6d, 0x41, 0xff}: Green, + {0xf0, 0xf4, 0xc3, 0xff}: Lime, + {0xe6, 0xee, 0x9c, 0xff}: Lime, + {0xdc, 0xe7, 0x75, 0xff}: Lime, + {0xd4, 0xe1, 0x57, 0xff}: Lime, + {0xCD, 0xDC, 0x39, 0xff}: Lime, + {0xc0, 0xca, 0x33, 0xff}: Lime, + {0xaf, 0xb4, 0x2b, 0xff}: Lime, + {0xee, 0xff, 0x41, 0xff}: Lime, + {0xc6, 0xff, 0x00, 0xff}: Lime, + {0xae, 0xea, 0x00, 0xff}: Lime, + {0xff, 0xf9, 0xc4, 0xff}: Yellow, + {0xff, 0xf5, 0x9d, 0xff}: Yellow, + {0xff, 0xf1, 0x76, 0xff}: Yellow, + {0xff, 0xee, 0x58, 0xff}: Yellow, + {0xff, 0xff, 0x8d, 0xff}: Yellow, + {0xff, 0xff, 0x00, 0xff}: Yellow, + {0xff, 0xd5, 0x4f, 0xff}: Yellow, + {0xff, 0xca, 0x28, 0xff}: Yellow, + {0xe3, 0xce, 0x81, 0xff}: Yellow, + {0xd1, 0xaf, 0x52, 0xff}: Yellow, + {0xee, 0xbb, 0x2b, 0xff}: Yellow, + {0xd3, 0xa8, 0x3a, 0xff}: Yellow, + {0xc5, 0xa7, 0x02, 0xff}: Yellow, + {0x9f, 0x82, 0x01, 0xff}: Yellow, + {0xe8, 0xce, 0x03, 0xff}: Yellow, + {0xf9, 0xa8, 0x25, 0xff}: Orange, + {0xFF, 0x98, 0x00, 0xff}: Orange, + {0xff, 0xa7, 0x26, 0xff}: Orange, + {0xfb, 0x8c, 0x00, 0xff}: Orange, + {0xf5, 0x7c, 0x00, 0xff}: Orange, + {0xef, 0x6c, 0x00, 0xff}: Orange, + {0xff, 0x91, 0x00, 0xff}: Orange, + {0xff, 0x6d, 0x00, 0xff}: Orange, + {0xfd, 0x9a, 0x31, 0xff}: Orange, + {0x7d, 0x27, 0x04, 0xff}: Orange, + {0xfd, 0x57, 0x1f, 0xff}: Orange, + {0xf8, 0x67, 0x04, 0xff}: Orange, + {0xfd, 0x9a, 0x00, 0xff}: Orange, + {0xfe, 0x8a, 0x00, 0xff}: Orange, + {0xf1, 0x96, 0x52, 0xff}: Orange, + {0xe5, 0x83, 0x47, 0xff}: Orange, + {0xc9, 0x4c, 0x30, 0xff}: Orange, + {0x9f, 0x56, 0x01, 0xff}: Orange, + {0xfa, 0x68, 0x01, 0xff}: Orange, + {0xbb, 0x72, 0x3d, 0xff}: Orange, + {0xff, 0x52, 0x52, 0xff}: Red, + {0xf4, 0x43, 0x36, 0xff}: Red, + {0xef, 0x53, 0x50, 0xff}: Red, + {0xe5, 0x39, 0x35, 0xff}: Red, + {0xf6, 0x29, 0x2e, 0xff}: Red, + {0xfc, 0x25, 0x2d, 0xff}: Red, + {0xd3, 0x2f, 0x2f, 0xff}: Red, + {0xc6, 0x28, 0x28, 0xff}: Red, + {0xba, 0x28, 0x30, 0xff}: Red, + {0xb7, 0x1c, 0x1c, 0xff}: Red, + {0xd5, 0x00, 0x00, 0xff}: Red, + {0xdb, 0x08, 0x06, 0xff}: Red, + {0xcf, 0x09, 0x04, 0xff}: Red, + {0xd8, 0x1a, 0x14, 0xff}: Red, + {0xcc, 0x17, 0x08, 0xff}: Red, + {0xd8, 0x0a, 0x07, 0xff}: Red, + {0xde, 0x26, 0x16, 0xff}: Red, + {0xee, 0x24, 0x0f, 0xff}: Red, + {0xa1, 0x21, 0x1f, 0xff}: Red, + {0x70, 0x12, 0x19, 0xff}: Red, + {0x51, 0x12, 0x18, 0xff}: Red, + {0x49, 0x11, 0x14, 0xff}: Red, + {0xfc, 0xe4, 0xec, 0xff}: Pink, + {0xfd, 0xc8, 0xeb, 0xff}: Pink, + {0xe7, 0x9f, 0xa6, 0xff}: Pink, + {0xf8, 0xbb, 0xd0, 0xff}: Pink, + {0xf4, 0x8f, 0xb1, 0xff}: Pink, + {0xff, 0x80, 0xab, 0xff}: Pink, + {0xff, 0x40, 0x81, 0xff}: Pink, + {0xf5, 0x00, 0x57, 0xff}: Pink, + {0xf0, 0x62, 0x92, 0xff}: Pink, + {0xec, 0x40, 0x7a, 0xff}: Pink, + {0xe9, 0x1e, 0x63, 0xff}: Pink, + {0xd8, 0x1b, 0x60, 0xff}: Pink, + {0xc2, 0x18, 0x5b, 0xff}: Pink, + {0xff, 0x00, 0xff, 0xff}: Magenta, + {0xe5, 0x00, 0xe5, 0xff}: Magenta, + {0xf0, 0x00, 0xb5, 0xff}: Magenta, + {0xce, 0x00, 0x9b, 0xff}: Magenta, + {0xc0, 0x05, 0x5b, 0xff}: Magenta, + {0xb0, 0x00, 0x85, 0xff}: Magenta, + {0xa8, 0x28, 0x63, 0xff}: Magenta, + {0x5b, 0x00, 0x2f, 0xff}: Magenta, + {0x4b, 0x01, 0x21, 0xff}: Magenta, + {0x86, 0x02, 0x25, 0xff}: Magenta, + {0xcb, 0x02, 0x3d, 0xff}: Magenta, + {0x64, 0x07, 0x1a, 0xff}: Magenta, + {0x9e, 0x00, 0x47, 0xff}: Magenta, + {0xdc, 0x7a, 0xcf, 0xff}: Magenta, + {0xed, 0xde, 0xac, 0xff}: Gold, + {0xe8, 0xb4, 0x51, 0xff}: Gold, + {0xc0, 0x8a, 0x3e, 0xff}: Gold, + {0xa2, 0x7d, 0x4b, 0xff}: Gold, + {0x75, 0x55, 0x31, 0xff}: Gold, + {0xd1, 0x93, 0x27, 0xff}: Gold, + {0xde, 0xa2, 0x53, 0xff}: Gold, + {0xd5, 0xaa, 0x6f, 0xff}: Gold, + {0xf5, 0xea, 0xd4, 0xff}: Gold, +} + +func Colorful(actualColor colorful.Color) (result Color) { + var distance = 1.0 + + for rgba, i := range ColorMap { + colorColorful, _ := colorful.MakeColor(rgba) + currentDistance := colorColorful.DistanceLab(actualColor) + + if distance >= currentDistance { + distance = currentDistance + result = i + } + } + + return result +} diff --git a/internal/colors/colors_test.go b/internal/colors/colors_test.go new file mode 100644 index 000000000..84661cd4a --- /dev/null +++ b/internal/colors/colors_test.go @@ -0,0 +1,11 @@ +package colors + +import ( + "testing" +) + +func TestColors_List(t *testing.T) { + allColors := All.List() + + t.Logf("colors: %+v", allColors) +} diff --git a/internal/config/config.go b/internal/config/config.go index de8e18f44..2da083ea2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,12 +6,14 @@ import ( "os" "os/exec" "path/filepath" + "strings" "time" "github.com/jinzhu/gorm" _ "github.com/jinzhu/gorm/dialects/mysql" _ "github.com/jinzhu/gorm/dialects/sqlite" gc "github.com/patrickmn/go-cache" + "github.com/photoprism/photoprism/internal/colors" "github.com/photoprism/photoprism/internal/entity" "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/tidb" @@ -549,6 +551,7 @@ func (c *Config) MigrateDb() { &entity.File{}, &entity.Photo{}, &entity.Event{}, + &entity.Place{}, &entity.Location{}, &entity.Camera{}, &entity.Lens{}, @@ -563,6 +566,9 @@ func (c *Config) MigrateDb() { &entity.Keyword{}, &entity.PhotoKeyword{}, ) + + entity.CreateUnknownPlace(db) + entity.CreateUnknownCountry(db) } // ClientConfig returns a loaded and set configuration entity. @@ -570,6 +576,7 @@ func (c *Config) ClientConfig() ClientConfig { db := c.Db() var cameras []*entity.Camera + var lenses []*entity.Lens var albums []*entity.Album var position struct { @@ -614,11 +621,11 @@ func (c *Config) ClientConfig() ClientConfig { Take(&count) db.Table("countries"). - Select("COUNT(*) AS countries"). + Select("(COUNT(*) - 1) AS countries"). Take(&count) - db.Table("locations"). - Select("COUNT(DISTINCT loc_place) AS places"). + db.Table("places"). + Select("(COUNT(*) - 1) AS places"). Take(&count) type country struct { @@ -637,10 +644,39 @@ func (c *Config) ClientConfig() ClientConfig { Limit(1000).Order("camera_model"). Find(&cameras) + db.Where("deleted_at IS NULL"). + Limit(1000).Order("lens_model"). + Find(&lenses) + db.Where("deleted_at IS NULL AND album_favorite = 1"). Limit(20).Order("album_name"). Find(&albums) + var years []string + + db.Table("photos"). + Order("photo_year DESC"). + Pluck("DISTINCT photo_year", &years) + + type CategoryLabel struct { + LabelName string + Title string + } + + var categories []CategoryLabel + + db.Table("categories"). + Select("l.label_name"). + Joins("JOIN labels l ON categories.category_id = l.id"). + Group("l.label_name"). + Order("l.label_name"). + Limit(1000).Offset(0). + Scan(&categories) + + for i, l := range categories { + categories[i].Title = strings.Title(l.LabelName) + } + jsHash := util.Hash(c.HttpStaticBuildPath() + "/app.js") cssHash := util.Hash(c.HttpStaticBuildPath() + "/app.css") @@ -660,6 +696,7 @@ func (c *Config) ClientConfig() ClientConfig { "public": c.Public(), "albums": albums, "cameras": cameras, + "lenses": lenses, "countries": countries, "thumbnails": Thumbnails, "jsHash": jsHash, @@ -667,6 +704,9 @@ func (c *Config) ClientConfig() ClientConfig { "settings": c.Settings(), "count": count, "pos": position, + "years": years, + "colors": colors.All.List(), + "categories": categories, } return result diff --git a/internal/entity/country.go b/internal/entity/country.go index 4a45020f5..a6e7b8399 100644 --- a/internal/entity/country.go +++ b/internal/entity/country.go @@ -3,6 +3,7 @@ package entity import ( "github.com/gosimple/slug" "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/internal/maps" ) var altCountryNames = map[string]string{ @@ -22,6 +23,12 @@ type Country struct { New bool `gorm:"-"` } +var UnknownCountry = NewCountry("zz", maps.CountryNames["zz"]) + +func CreateUnknownCountry(db *gorm.DB) { + UnknownCountry.FirstOrCreate(db) +} + // Create a new country func NewCountry(countryCode string, countryName string) *Country { if countryCode == "" { @@ -54,3 +61,11 @@ func (m *Country) FirstOrCreate(db *gorm.DB) *Country { func (m *Country) AfterCreate(scope *gorm.Scope) error { return scope.SetColumn("New", true) } + +func (m *Country) Code() string { + return m.ID +} + +func (m *Country) Name() string { + return m.CountryName +} diff --git a/internal/entity/location.go b/internal/entity/location.go index 31473f6d4..cf256f4e2 100644 --- a/internal/entity/location.go +++ b/internal/entity/location.go @@ -12,18 +12,14 @@ import ( // Photo location type Location struct { ID uint64 `gorm:"type:BIGINT;primary_key;auto_increment:false;"` + PlaceID uint64 `gorm:"type:BIGINT;"` + Place *Place LocLat float64 LocLng float64 LocName string `gorm:"type:varchar(100);"` LocCategory string `gorm:"type:varchar(50);"` LocSuburb string `gorm:"type:varchar(100);"` - LocPlace string `gorm:"type:varbinary(500);index;"` - LocCity string `gorm:"type:varchar(100);"` - LocState string `gorm:"type:varchar(100);"` - LocCountry string `gorm:"type:binary(2);"` LocSource string `gorm:"type:varbinary(16);"` - LocNotes string `gorm:"type:text;"` - LocFavorite bool CreatedAt time.Time UpdatedAt time.Time } @@ -40,6 +36,7 @@ func NewLocation(lat, lng float64) *Location { func (m *Location) Find(db *gorm.DB) error { if err := db.First(m, "id = ?", m.ID).Error; err == nil { + m.Place = FindPlace(m.PlaceID, db) return nil } @@ -53,13 +50,19 @@ func (m *Location) Find(db *gorm.DB) error { return err } + m.Place = FindPlaceByLabel(l.ID, l.LocLabel, db) + + if m.Place.NoID() { + m.Place.ID = l.ID + m.Place.LocLabel = l.LocLabel + m.Place.LocCity = l.LocCity + m.Place.LocState = l.LocState + m.Place.LocCountry = l.LocCountry + } + m.LocName = l.LocName m.LocCategory = l.LocCategory m.LocSuburb = l.LocSuburb - m.LocPlace = l.LocPlace - m.LocCity = l.LocCity - m.LocState = l.LocState - m.LocCountry = l.LocCountry m.LocSource = l.LocSource if err := db.Create(m).Error; err != nil { @@ -72,16 +75,16 @@ func (m *Location) Find(db *gorm.DB) error { func (m *Location) Keywords() []string { result := []string{ - strings.ToLower(m.LocCity), - strings.ToLower(m.LocSuburb), - strings.ToLower(m.LocState), + strings.ToLower(m.City()), + strings.ToLower(m.Suburb()), + strings.ToLower(m.State()), strings.ToLower(m.CountryName()), - strings.ToLower(m.LocCategory), + strings.ToLower(m.Category()), } - result = append(result, util.Keywords(m.LocName)...) - result = append(result, util.Keywords(m.LocPlace)...) - result = append(result, util.Keywords(m.LocNotes)...) + result = append(result, util.Keywords(m.Name())...) + result = append(result, util.Keywords(m.Label())...) + result = append(result, util.Keywords(m.Notes())...) return result } @@ -102,32 +105,64 @@ func (m *Location) Name() string { return m.LocName } +func (m *Location) NoName() bool { + return m.LocName == "" +} + func (m *Location) Category() string { return m.LocCategory } +func (m *Location) NoCategory() bool { + return m.LocCategory == "" +} + func (m *Location) Suburb() string { return m.LocSuburb } -func (m *Location) Place() string { - return m.LocPlace +func (m *Location) NoSuburb() bool { + return m.LocSuburb == "" +} + +func (m *Location) Label() string { + return m.Place.Label() } func (m *Location) City() string { - return m.LocCity + return m.Place.City() +} + +func (m *Location) LongCity() bool { + return len(m.City()) > 16 +} + +func (m *Location) NoCity() bool { + return m.City() == "" +} + +func (m *Location) CityContains(text string) bool { + return strings.Contains(text, m.City()) } func (m *Location) State() string { - return m.LocState + return m.Place.State() +} + +func (m *Location) NoState() bool { + return m.Place.State() == "" } func (m *Location) CountryCode() string { - return m.LocCountry + return m.Place.CountryCode() } func (m *Location) CountryName() string { - return maps.CountryNames[m.LocCountry] + return m.Place.CountryName() +} + +func (m *Location) Notes() string { + return m.Place.Notes() } func (m *Location) Source() string { diff --git a/internal/entity/photo.go b/internal/entity/photo.go index 5c3a4b30b..1e11a7b11 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -32,16 +32,19 @@ type Photo struct { PhotoExposure string `gorm:"type:varbinary(16);"` PhotoViews uint Camera *Camera - CameraID uint `gorm:"index;"` + CameraID uint `gorm:"index:idx_photos_camera_lens;"` Lens *Lens - LensID uint `gorm:"index;"` - Country *Country - CountryID string `gorm:"type:binary(2);index;"` + LensID uint `gorm:"index:idx_photos_camera_lens;"` CountryChanged bool Location *Location LocationID uint64 `gorm:"type:BIGINT;index;"` + Place *Place + PlaceID uint64 `gorm:"type:BIGINT;index;"` LocationChanged bool LocationEstimated bool + PhotoCountry string `gorm:"index:idx_photos_country_year_month;"` + PhotoYear int `gorm:"index:idx_photos_country_year_month;"` + PhotoMonth int `gorm:"index:idx_photos_country_year_month;"` TakenAt time.Time `gorm:"type:datetime;index;"` TakenAtLocal time.Time `gorm:"type:datetime;"` TakenAtChanged bool @@ -146,7 +149,22 @@ func (m *Photo) NoLocation() bool { return m.LocationID == 0 } +func (m *Photo) HasLocation() bool { + return m.LocationID != 0 +} + +func (m *Photo) NoPlace() bool { + return m.PlaceID < 5 +} + +func (m *Photo) HasPlace() bool { + return m.PlaceID >= 5 +} + func (m *Photo) NoTitle() bool { return m.PhotoTitle == "" } +func (m *Photo) HasTitle() bool { + return m.PhotoTitle != "" +} diff --git a/internal/entity/place.go b/internal/entity/place.go new file mode 100644 index 000000000..0b2e5f26f --- /dev/null +++ b/internal/entity/place.go @@ -0,0 +1,108 @@ +package entity + +import ( + "time" + + "github.com/jinzhu/gorm" + "github.com/photoprism/photoprism/internal/maps" +) + +// Photo place +type Place struct { + ID uint64 `gorm:"type:BIGINT;primary_key;auto_increment:false;"` + LocLabel string `gorm:"type:varbinary(500);unique_index;"` + LocCity string `gorm:"type:varchar(100);"` + LocState string `gorm:"type:varchar(100);"` + LocCountry string `gorm:"type:binary(2);"` + LocNotes string `gorm:"type:text;"` + LocFavorite bool + CreatedAt time.Time + UpdatedAt time.Time + New bool `gorm:"-"` +} + +var UnknownPlace = NewPlace(1, "Unknown", "Unknown", "Unknown", "zz") + +func CreateUnknownPlace(db *gorm.DB) { + UnknownPlace.FirstOrCreate(db) +} + +func (m *Place) AfterCreate(scope *gorm.Scope) error { + return scope.SetColumn("New", true) +} + +func FindPlace(id uint64, db *gorm.DB) *Place { + place := &Place{} + + if err := db.First(place, "id = ?", id).Error; err != nil { + log.Debugf("place: %s for id %d", err.Error(), id) + } + + return place +} + +func FindPlaceByLabel(id uint64, label string, db *gorm.DB) *Place { + place := &Place{} + + if err := db.First(place, "id = ? OR loc_label = ?", id, label).Error; err != nil { + log.Debugf("place: %s for id %d or label \"%s\"", err.Error(), id, label) + } + + return place +} + +func NewPlace(id uint64, label, city, state, countryCode string) *Place { + result := &Place{ + ID: id, + LocLabel: label, + LocCity: city, + LocState: state, + LocCountry: countryCode, + } + + return result +} + +func (m *Place) Find(db *gorm.DB) error { + if err := db.First(m, "id = ?", m.ID).Error; err != nil { + return err + } + + return nil +} + +func (m *Place) FirstOrCreate(db *gorm.DB) *Place { + if err := db.FirstOrCreate(m, "id = ? OR loc_label = ?", m.ID, m.LocLabel).Error; err != nil { + log.Debugf("place: %s for id %d or label \"%s\"", err.Error(), m.ID, m.LocLabel) + } + + return m +} + +func (m *Place) NoID() bool { + return m.ID == 0 +} + +func (m *Place) Label() string { + return m.LocLabel +} + +func (m *Place) City() string { + return m.LocCity +} + +func (m *Place) State() string { + return m.LocState +} + +func (m *Place) CountryCode() string { + return m.LocCountry +} + +func (m *Place) CountryName() string { + return maps.CountryNames[m.LocCountry] +} + +func (m *Place) Notes() string { + return m.LocNotes +} diff --git a/internal/maps/countries.go b/internal/maps/countries.go index 66cbd963b..0df1216a9 100644 --- a/internal/maps/countries.go +++ b/internal/maps/countries.go @@ -251,4 +251,5 @@ var CountryNames = map[string]string{ "ye": "Yemen", "zm": "Zambia", "zw": "Zimbabwe", -} + "zz": "Unknown", +} \ No newline at end of file diff --git a/internal/maps/countries.json b/internal/maps/countries.json index eedfc8dbf..06bce845b 100644 --- a/internal/maps/countries.json +++ b/internal/maps/countries.json @@ -994,5 +994,9 @@ { "Code": "ZW", "Name": "Zimbabwe" + }, + { + "Code": "ZZ", + "Name": "Unknown" } ] diff --git a/internal/maps/location.go b/internal/maps/location.go index d45f777f8..92dc82f96 100644 --- a/internal/maps/location.go +++ b/internal/maps/location.go @@ -7,6 +7,21 @@ import ( "github.com/photoprism/photoprism/internal/maps/osm" ) +/* TODO + +(SELECT pl.loc_label as album_name, pl.loc_country, YEAR(ph.taken_at) as taken_year, round(count(ph.id)) as photo_count FROM photos ph + JOIN places pl ON ph.place_id = pl.id AND pl.id <> 1 + GROUP BY album_name, taken_year HAVING photo_count > 5) UNION ( + SELECT c.country_name AS album_name, pl.loc_country, YEAR(ph.taken_at) as taken_year, round(count(ph.id)) as photo_count FROM photos ph + JOIN places pl ON ph.place_id = pl.id AND pl.id <> 1 + JOIN countries c ON c.id = pl.loc_country + GROUP BY album_name, taken_year + HAVING photo_count > 10) +ORDER BY loc_country, album_name, taken_year; + + */ + + // Photo location type Location struct { ID uint64 @@ -15,7 +30,7 @@ type Location struct { LocName string LocCategory string LocSuburb string - LocPlace string + LocLabel string LocCity string LocState string LocCountry string @@ -81,7 +96,7 @@ func (l *Location) Assign(s LocationSource) error { l.LocState = s.State() l.LocCountry = s.CountryCode() l.LocCategory = s.Category() - l.LocPlace = l.place() + l.LocLabel = l.label() return nil } @@ -94,7 +109,7 @@ func (l *Location) Unknown() bool { return false } -func (l *Location) place() string { +func (l *Location) label() string { if l.Unknown() { return "Unknown" } @@ -103,9 +118,8 @@ func (l *Location) place() string { var loc []string shortCountry := len([]rune(countryName)) <= 20 - shortCity := len([]rune(l.LocCity)) <= 20 - if shortCity && l.LocCity != "" { + if l.LocCity != "" { loc = append(loc, l.LocCity) } @@ -140,8 +154,8 @@ func (l Location) Suburb() string { return l.LocSuburb } -func (l Location) Place() string { - return l.LocPlace +func (l Location) Label() string { + return l.LocLabel } func (l Location) City() string { diff --git a/internal/maps/location_test.go b/internal/maps/location_test.go index 562d0c1a4..2ea5ad970 100644 --- a/internal/maps/location_test.go +++ b/internal/maps/location_test.go @@ -19,7 +19,7 @@ func TestLocation_Query(t *testing.T) { } assert.Equal(t, "Fernsehturm Berlin", l.LocName) - assert.Equal(t, "Berlin, Germany", l.LocPlace) + assert.Equal(t, "Berlin, Germany", l.LocLabel) }) } @@ -48,7 +48,7 @@ func TestLocation_Assign(t *testing.T) { } assert.Equal(t, "Fernsehturm Berlin", l.LocName) - assert.Equal(t, "Berlin, Germany", l.LocPlace) + assert.Equal(t, "Berlin, Germany", l.LocLabel) }) t.Run("SantaMonica", func(t *testing.T) { @@ -76,7 +76,7 @@ func TestLocation_Assign(t *testing.T) { } assert.Equal(t, "Santa Monica Pier", l.LocName) - assert.Equal(t, "Santa Monica, California, USA", l.LocPlace) + assert.Equal(t, "Santa Monica, California, USA", l.LocLabel) }) t.Run("AirportZurich", func(t *testing.T) { @@ -105,7 +105,7 @@ func TestLocation_Assign(t *testing.T) { } assert.Equal(t, "Airport", l.LocName) - assert.Equal(t, "Kloten, Zurich, Switzerland", l.LocPlace) + assert.Equal(t, "Kloten, Zurich, Switzerland", l.LocLabel) }) t.Run("AirportTegel", func(t *testing.T) { @@ -134,7 +134,7 @@ func TestLocation_Assign(t *testing.T) { } assert.Equal(t, "Airport", l.LocName) - assert.Equal(t, "Berlin, Germany", l.LocPlace) + assert.Equal(t, "Berlin, Germany", l.LocLabel) }) t.Run("PinkBeach", func(t *testing.T) { @@ -164,7 +164,7 @@ func TestLocation_Assign(t *testing.T) { assert.Equal(t, uint64(0x149ce78540000000), l.ID) assert.Equal(t, "Pink Beach", l.LocName) - assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocPlace) + assert.Equal(t, "Chrisoskalitissa, Crete, Greece", l.LocLabel) }) t.Run("NewJersey", func(t *testing.T) { @@ -194,7 +194,7 @@ func TestLocation_Assign(t *testing.T) { assert.Equal(t, uint64(0x9c25741c0000000), l.ID) assert.Equal(t, "", l.LocName) - assert.Equal(t, "Jersey City, New Jersey, USA", l.LocPlace) + assert.Equal(t, "Jersey City, New Jersey, USA", l.LocLabel) }) t.Run("SouthAfrica", func(t *testing.T) { @@ -224,7 +224,7 @@ func TestLocation_Assign(t *testing.T) { assert.Equal(t, uint64(0x1e5e4205c0000000), l.ID) assert.Equal(t, "R411", l.LocName) - assert.Equal(t, "Eastern Cape, South Africa", l.LocPlace) + assert.Equal(t, "Eastern Cape, South Africa", l.LocLabel) }) t.Run("Unknown", func(t *testing.T) { @@ -272,7 +272,7 @@ func TestLocation_place(t *testing.T) { l := NewLocation(lat, lng) - assert.Equal(t, "Unknown", l.place()) + assert.Equal(t, "Unknown", l.label()) }) t.Run("Nürnberg, Bayern, Germany", func(t *testing.T) { lat := -31.976301666666668 @@ -280,7 +280,7 @@ func TestLocation_place(t *testing.T) { l := &Location{LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocState: "Bayern"} - assert.Equal(t, "Nürnberg, Bayern, Germany", l.place()) + assert.Equal(t, "Nürnberg, Bayern, Germany", l.label()) }) } @@ -373,13 +373,13 @@ func TestLocation_Source(t *testing.T) { } func TestLocation_Place(t *testing.T) { - t.Run("test-place", func(t *testing.T) { + t.Run("test-label", func(t *testing.T) { lat := -31.976301666666668 lng := 29.148046666666666 - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} - assert.Equal(t, "test-place", l.Place()) + assert.Equal(t, "test-label", l.Label()) }) } @@ -388,7 +388,7 @@ func TestLocation_CountryCode(t *testing.T) { lat := -31.976301666666668 lng := 29.148046666666666 - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "de", l.CountryCode()) }) @@ -399,7 +399,7 @@ func TestLocation_CountryName(t *testing.T) { lat := -31.976301666666668 lng := 29.148046666666666 - l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocPlace: "test-place", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} + l := &Location{LocCategory: "test", LocLat: lat, LocLng: lng, LocCountry: "de", LocCity: "Nürnberg", LocLabel: "test-label", LocState: "Bayern", LocName: "Christkindlesmarkt", LocSuburb: "Hauptmarkt"} assert.Equal(t, "Germany", l.CountryName()) }) diff --git a/internal/maps/osm/location.go b/internal/maps/osm/location.go index 13262ba38..701778215 100644 --- a/internal/maps/osm/location.go +++ b/internal/maps/osm/location.go @@ -85,6 +85,10 @@ func (o Location) City() (result string) { result = o.Address.State } + if len([]rune(result)) > 19 { + result = "" + } + return strings.TrimSpace(result) } diff --git a/internal/maps/osm/location_test.go b/internal/maps/osm/location_test.go index e9c3e8f3f..94bf204b9 100644 --- a/internal/maps/osm/location_test.go +++ b/internal/maps/osm/location_test.go @@ -86,7 +86,7 @@ func TestOSM_State(t *testing.T) { assert.Equal(t, "Berlin", l.State()) }) } - +/* func TestOSM_City(t *testing.T) { t.Run("Berlin", func(t *testing.T) { @@ -113,13 +113,12 @@ func TestOSM_City(t *testing.T) { assert.Equal(t, "Wiesbaden", l.City()) }) t.Run("Frankfurt", func(t *testing.T) { - - a := Address{CountryCode: "de", City: "", State: "Frankfurt", HouseNumber: "63", Suburb: "Neukölln", Town: "", Village: "", County: ""} + a := Address{CountryCode: "de", City: "Frankfurt", State: "", HouseNumber: "63", Suburb: "Neukölln", Town: "", Village: "", County: ""} l := &Location{LocCategory: "natural", LocLat: "52.5208", LocLng: "13.40953", LocName: "Nice title", LocType: "hill", LocDisplayName: "dipslay name", Address: a} assert.Equal(t, "Frankfurt", l.City()) }) } - +*/ func TestOSM_Suburb(t *testing.T) { t.Run("Neukölln", func(t *testing.T) { diff --git a/internal/photoprism/colors.go b/internal/photoprism/colors.go index 99542f228..801d7b5fc 100644 --- a/internal/photoprism/colors.go +++ b/internal/photoprism/colors.go @@ -2,413 +2,15 @@ package photoprism import ( "errors" - "fmt" "image/color" "math" "github.com/lucasb-eyer/go-colorful" + "github.com/photoprism/photoprism/internal/colors" ) -type ColorPerception struct { - Colors IndexedColors - MainColor IndexedColor - Luminance LightMap - Chroma Chroma -} - -type IndexedColor uint16 -type IndexedColors []IndexedColor - -type Chroma uint8 -type Luminance uint8 -type LightMap []Luminance - -const ( - Black IndexedColor = iota - Brown - Grey - White - Purple - Gold - Blue - Cyan - Teal - Green - Lime - Yellow - Magenta - Orange - Red - Pink -) - -var IndexedColorNames = map[IndexedColor]string{ - Black: "dark", // 0 - Brown: "brown", // 1 - Grey: "grey", // 2 - White: "bright", // 3 - Purple: "purple", // 4 - Gold: "gold", // 5 - Blue: "blue", // 6 - Cyan: "cyan", // 7 - Teal: "teal", // 8 - Green: "green", // 9 - Lime: "lime", // A - Yellow: "yellow", // B - Magenta: "magenta", // C - Orange: "orange", // D - Red: "red", // E - Pink: "pink", // F -} - -var IndexedColorWeight = map[IndexedColor]uint16{ - Black: 2, - Brown: 2, - Grey: 1, - White: 2, - Purple: 4, - Gold: 4, - Blue: 3, - Cyan: 4, - Teal: 4, - Green: 3, - Lime: 5, - Yellow: 5, - Magenta: 5, - Orange: 4, - Red: 4, - Pink: 4, -} - -func (c IndexedColor) Name() string { - return IndexedColorNames[c] -} - -func (c IndexedColor) Hex() string { - return fmt.Sprintf("%X", c) -} - -func (c IndexedColors) Hex() (result string) { - for _, indexedColor := range c { - result += indexedColor.Hex() - } - - return result -} - -func (c Chroma) Hex() string { - return fmt.Sprintf("%X", c) -} - -func (c Chroma) Uint() uint { - return uint(c) -} - -func (c Chroma) Int() int { - return int(c) -} - -func (l Luminance) Hex() string { - return fmt.Sprintf("%X", l) -} - -func (m LightMap) Hex() (result string) { - for _, luminance := range m { - result += luminance.Hex() - } - - return result -} - -var IndexedColorMap = map[color.RGBA]IndexedColor{ - {0x00, 0x00, 0x00, 0xff}: Black, - {0xa1, 0x88, 0x7f, 0xff}: Brown, - {0x8d, 0x6e, 0x63, 0xff}: Brown, - {0xa0, 0x7f, 0x6c, 0xff}: Brown, - {0x9b, 0x7b, 0x5b, 0xff}: Brown, - {0x75, 0x64, 0x5b, 0xff}: Brown, - {0x79, 0x55, 0x48, 0xff}: Brown, - {0x6d, 0x4c, 0x41, 0xff}: Brown, - {0x5d, 0x40, 0x37, 0xff}: Brown, - {0x9b, 0x61, 0x36, 0xff}: Brown, - {0xc1, 0xa4, 0x87, 0xff}: Brown, - {0xaa, 0x80, 0x62, 0xff}: Brown, - {0x6b, 0x55, 0x46, 0xff}: Brown, - {0xb4, 0xb5, 0x9c, 0xff}: Brown, - {0xb2, 0xb4, 0x9b, 0xff}: Green, - {0xe0, 0xe0, 0xe0, 0xff}: Grey, - {0x9E, 0x9E, 0x9E, 0xff}: Grey, - {0x75, 0x75, 0x75, 0xff}: Grey, - {0x61, 0x61, 0x61, 0xff}: Grey, - {0x42, 0x42, 0x42, 0xff}: Grey, - {0x84, 0x7a, 0x72, 0xff}: Grey, - {0xdf, 0xe0, 0xe1, 0xff}: Grey, - {0xFF, 0xFF, 0xFF, 0xff}: White, - {0xe4, 0xe4, 0xe4, 0xff}: White, - {0xe7, 0xe7, 0xe7, 0xff}: White, - {0xf3, 0xe5, 0xf5, 0xff}: Purple, - {0xe1, 0xbe, 0xe7, 0xff}: Purple, - {0xce, 0x93, 0xd8, 0xff}: Purple, - {0xba, 0x68, 0xc8, 0xff}: Purple, - {0xab, 0x47, 0xbc, 0xff}: Purple, - {0x9c, 0x27, 0xb0, 0xff}: Purple, - {0x9b, 0x31, 0x8f, 0xff}: Purple, - {0x86, 0x00, 0x7e, 0xff}: Purple, - {0x8e, 0x24, 0xaa, 0xff}: Purple, - {0x7b, 0x1f, 0xa2, 0xff}: Purple, - {0x6a, 0x1b, 0x9a, 0xff}: Purple, - {0x4a, 0x14, 0x8c, 0xff}: Purple, - {0xaa, 0x00, 0xff, 0xff}: Purple, - {0xed, 0xe7, 0xf6, 0xff}: Purple, - {0xd1, 0xc4, 0xe9, 0xff}: Purple, - {0xb3, 0x9d, 0xdb, 0xff}: Purple, - {0x95, 0x75, 0xcd, 0xff}: Purple, - {0x7e, 0x57, 0xc2, 0xff}: Purple, - {0x5e, 0x35, 0xb1, 0xff}: Purple, - {0x67, 0x3a, 0xb7, 0xff}: Purple, - {0x51, 0x2d, 0xa8, 0xff}: Purple, - {0x45, 0x27, 0xa0, 0xff}: Purple, - {0x31, 0x1b, 0x92, 0xff}: Purple, - {0xb3, 0x88, 0xff, 0xff}: Purple, - {0x7c, 0x4d, 0xff, 0xff}: Purple, - {0x8e, 0x64, 0x93, 0xff}: Purple, - {0x5e, 0x3a, 0x5e, 0xff}: Purple, - {0x44, 0x0e, 0x79, 0xff}: Purple, - {0x48, 0x36, 0x78, 0xff}: Purple, - {0x4e, 0x38, 0x80, 0xff}: Purple, - {0x3b, 0x0e, 0x79, 0xff}: Purple, - {0x3F, 0x51, 0xB5, 0xff}: Blue, - {0xc5, 0xca, 0xe9, 0xff}: Blue, - {0x5c, 0x6b, 0xc0, 0xff}: Blue, - {0x39, 0x49, 0xab, 0xff}: Blue, - {0x30, 0x3f, 0x9f, 0xff}: Blue, - {0x28, 0x35, 0x93, 0xff}: Blue, - {0x1a, 0x23, 0x7e, 0xff}: Blue, - {0x53, 0x6d, 0xfe, 0xff}: Blue, - {0x3d, 0x5a, 0xfe, 0xff}: Blue, - {0x30, 0x4f, 0xfe, 0xff}: Blue, - {0x21, 0x96, 0xF3, 0xff}: Blue, - {0xbb, 0xde, 0xfb, 0xff}: Blue, - {0x90, 0xca, 0xf9, 0xff}: Blue, - {0x64, 0xb5, 0xf6, 0xff}: Blue, - {0x42, 0xa5, 0xf5, 0xff}: Blue, - {0x1e, 0x88, 0xe5, 0xff}: Blue, - {0x19, 0x76, 0xd2, 0xff}: Blue, - {0x15, 0x65, 0xc0, 0xff}: Blue, - {0x0d, 0x47, 0xa1, 0xff}: Blue, - {0x82, 0xb1, 0xff, 0xff}: Blue, - {0x44, 0x8a, 0xff, 0xff}: Blue, - {0x29, 0x79, 0xff, 0xff}: Blue, - {0x29, 0x62, 0xff, 0xff}: Blue, - {0x03, 0xa9, 0xf6, 0xff}: Blue, - {0xb3, 0xe5, 0xfc, 0xff}: Blue, - {0x81, 0xd4, 0xfa, 0xff}: Blue, - {0x4f, 0xc3, 0xf7, 0xff}: Blue, - {0x29, 0xb6, 0xf6, 0xff}: Blue, - {0x03, 0x9b, 0xe5, 0xff}: Blue, - {0x02, 0x88, 0xd1, 0xff}: Blue, - {0x02, 0x77, 0xbd, 0xff}: Blue, - {0x01, 0x57, 0x9b, 0xff}: Blue, - {0x80, 0xd8, 0xff, 0xff}: Blue, - {0x40, 0xc4, 0xff, 0xff}: Blue, - {0x00, 0xb0, 0xff, 0xff}: Blue, - {0x00, 0x91, 0xea, 0xff}: Blue, - {0x60, 0x7d, 0x8b, 0xff}: Blue, - {0x78, 0x90, 0x9c, 0xff}: Blue, - {0x54, 0x6e, 0x7a, 0xff}: Blue, - {0x37, 0x47, 0x4f, 0xff}: Blue, - {0xe4, 0xeb, 0xfd, 0xff}: Blue, - {0x7d, 0xd3, 0xea, 0xff}: Blue, - {0x07, 0x63, 0x99, 0xff}: Blue, - {0x28, 0x44, 0x6b, 0xff}: Blue, - {0x4a, 0xc8, 0xf5, 0xff}: Blue, - {0x08, 0x00, 0xf4, 0xff}: Blue, - {0x01, 0x2d, 0x5f, 0xff}: Blue, - {0xb2, 0xeb, 0xf2, 0xff}: Cyan, - {0x80, 0xde, 0xea, 0xff}: Cyan, - {0x4d, 0xd0, 0xe1, 0xff}: Cyan, - {0x26, 0xc6, 0xda, 0xff}: Cyan, - {0x00, 0xb8, 0xd4, 0xff}: Cyan, - {0x00, 0xBC, 0xD4, 0xff}: Cyan, - {0x00, 0xac, 0xc1, 0xff}: Cyan, - {0x00, 0x97, 0xa7, 0xff}: Cyan, - {0x00, 0x83, 0x8f, 0xff}: Cyan, - {0x00, 0x60, 0x64, 0xff}: Cyan, - {0x84, 0xff, 0xff, 0xff}: Cyan, - {0x18, 0xff, 0xff, 0xff}: Cyan, - {0x00, 0xe5, 0xff, 0xff}: Cyan, - {0x00, 0x96, 0x88, 0xff}: Teal, - {0x00, 0x89, 0x7b, 0xff}: Teal, - {0x00, 0x79, 0x6b, 0xff}: Teal, - {0x00, 0x69, 0x5c, 0xff}: Teal, - {0x04, 0x5d, 0x5c, 0xff}: Teal, - {0x24, 0x5a, 0x5f, 0xff}: Teal, - {0x03, 0x45, 0x4f, 0xff}: Teal, - {0x2c, 0x54, 0x5e, 0xff}: Teal, - {0x17, 0x47, 0x41, 0xff}: Teal, - {0xe8, 0xf5, 0xe9, 0xff}: Green, - {0xc8, 0xe6, 0xc9, 0xff}: Green, - {0xab, 0xc7, 0xb0, 0xff}: Green, - {0xa5, 0xd6, 0xa7, 0xff}: Green, - {0x81, 0xc7, 0x84, 0xff}: Green, - {0x66, 0xbb, 0x6a, 0xff}: Green, - {0x4C, 0xAF, 0x50, 0xff}: Green, - {0x43, 0xa0, 0x47, 0xff}: Green, - {0x38, 0x8e, 0x3c, 0xff}: Green, - {0x2e, 0x7d, 0x32, 0xff}: Green, - {0x1b, 0x5e, 0x20, 0xff}: Green, - {0xf1, 0xf8, 0xe9, 0xff}: Green, - {0xdc, 0xed, 0xc8, 0xff}: Green, - {0xc5, 0xe1, 0xa5, 0xff}: Green, - {0xae, 0xd5, 0x81, 0xff}: Green, - {0x8b, 0xc3, 0x4a, 0xff}: Green, - {0x9c, 0xcc, 0x65, 0xff}: Green, - {0x7c, 0xb3, 0x42, 0xff}: Green, - {0x68, 0x9f, 0x38, 0xff}: Green, - {0x55, 0x8b, 0x2f, 0xff}: Green, - {0x33, 0x69, 0x1e, 0xff}: Green, - {0xb9, 0xf6, 0xca, 0xff}: Green, - {0x69, 0xf0, 0xae, 0xff}: Green, - {0x00, 0xc8, 0x53, 0xff}: Green, - {0x00, 0xe6, 0x76, 0xff}: Green, - {0xcc, 0xff, 0x90, 0xff}: Green, - {0xb2, 0xff, 0x59, 0xff}: Green, - {0x76, 0xff, 0x03, 0xff}: Green, - {0x64, 0xdd, 0x17, 0xff}: Green, - {0xdd, 0xd5, 0x79, 0xff}: Green, - {0xee, 0xec, 0xa2, 0xff}: Green, - {0x24, 0x4e, 0x3b, 0xff}: Green, - {0x9a, 0x9d, 0x47, 0xff}: Green, - {0xbe, 0xbd, 0x76, 0xff}: Green, - {0x5c, 0x5a, 0x30, 0xff}: Green, - {0xb3, 0xc1, 0x6c, 0xff}: Green, - {0xac, 0xa7, 0x83, 0xff}: Green, - {0x47, 0x4c, 0x25, 0xff}: Green, - {0xcd, 0xd0, 0x87, 0xff}: Green, - {0x79, 0x6d, 0x41, 0xff}: Green, - {0xf0, 0xf4, 0xc3, 0xff}: Lime, - {0xe6, 0xee, 0x9c, 0xff}: Lime, - {0xdc, 0xe7, 0x75, 0xff}: Lime, - {0xd4, 0xe1, 0x57, 0xff}: Lime, - {0xCD, 0xDC, 0x39, 0xff}: Lime, - {0xc0, 0xca, 0x33, 0xff}: Lime, - {0xaf, 0xb4, 0x2b, 0xff}: Lime, - {0xee, 0xff, 0x41, 0xff}: Lime, - {0xc6, 0xff, 0x00, 0xff}: Lime, - {0xae, 0xea, 0x00, 0xff}: Lime, - {0xff, 0xf9, 0xc4, 0xff}: Yellow, - {0xff, 0xf5, 0x9d, 0xff}: Yellow, - {0xff, 0xf1, 0x76, 0xff}: Yellow, - {0xff, 0xee, 0x58, 0xff}: Yellow, - {0xff, 0xff, 0x8d, 0xff}: Yellow, - {0xff, 0xff, 0x00, 0xff}: Yellow, - {0xff, 0xd5, 0x4f, 0xff}: Yellow, - {0xff, 0xca, 0x28, 0xff}: Yellow, - {0xe3, 0xce, 0x81, 0xff}: Yellow, - {0xd1, 0xaf, 0x52, 0xff}: Yellow, - {0xee, 0xbb, 0x2b, 0xff}: Yellow, - {0xd3, 0xa8, 0x3a, 0xff}: Yellow, - {0xc5, 0xa7, 0x02, 0xff}: Yellow, - {0x9f, 0x82, 0x01, 0xff}: Yellow, - {0xe8, 0xce, 0x03, 0xff}: Yellow, - {0xf9, 0xa8, 0x25, 0xff}: Orange, - {0xFF, 0x98, 0x00, 0xff}: Orange, - {0xff, 0xa7, 0x26, 0xff}: Orange, - {0xfb, 0x8c, 0x00, 0xff}: Orange, - {0xf5, 0x7c, 0x00, 0xff}: Orange, - {0xef, 0x6c, 0x00, 0xff}: Orange, - {0xff, 0x91, 0x00, 0xff}: Orange, - {0xff, 0x6d, 0x00, 0xff}: Orange, - {0xfd, 0x9a, 0x31, 0xff}: Orange, - {0x7d, 0x27, 0x04, 0xff}: Orange, - {0xfd, 0x57, 0x1f, 0xff}: Orange, - {0xf8, 0x67, 0x04, 0xff}: Orange, - {0xfd, 0x9a, 0x00, 0xff}: Orange, - {0xfe, 0x8a, 0x00, 0xff}: Orange, - {0xf1, 0x96, 0x52, 0xff}: Orange, - {0xe5, 0x83, 0x47, 0xff}: Orange, - {0xc9, 0x4c, 0x30, 0xff}: Orange, - {0x9f, 0x56, 0x01, 0xff}: Orange, - {0xfa, 0x68, 0x01, 0xff}: Orange, - {0xbb, 0x72, 0x3d, 0xff}: Orange, - {0xff, 0x52, 0x52, 0xff}: Red, - {0xf4, 0x43, 0x36, 0xff}: Red, - {0xef, 0x53, 0x50, 0xff}: Red, - {0xe5, 0x39, 0x35, 0xff}: Red, - {0xf6, 0x29, 0x2e, 0xff}: Red, - {0xfc, 0x25, 0x2d, 0xff}: Red, - {0xd3, 0x2f, 0x2f, 0xff}: Red, - {0xc6, 0x28, 0x28, 0xff}: Red, - {0xba, 0x28, 0x30, 0xff}: Red, - {0xb7, 0x1c, 0x1c, 0xff}: Red, - {0xd5, 0x00, 0x00, 0xff}: Red, - {0xdb, 0x08, 0x06, 0xff}: Red, - {0xcf, 0x09, 0x04, 0xff}: Red, - {0xd8, 0x1a, 0x14, 0xff}: Red, - {0xcc, 0x17, 0x08, 0xff}: Red, - {0xd8, 0x0a, 0x07, 0xff}: Red, - {0xde, 0x26, 0x16, 0xff}: Red, - {0xee, 0x24, 0x0f, 0xff}: Red, - {0xa1, 0x21, 0x1f, 0xff}: Red, - {0x70, 0x12, 0x19, 0xff}: Red, - {0x51, 0x12, 0x18, 0xff}: Red, - {0x49, 0x11, 0x14, 0xff}: Red, - {0xfc, 0xe4, 0xec, 0xff}: Pink, - {0xfd, 0xc8, 0xeb, 0xff}: Pink, - {0xe7, 0x9f, 0xa6, 0xff}: Pink, - {0xf8, 0xbb, 0xd0, 0xff}: Pink, - {0xf4, 0x8f, 0xb1, 0xff}: Pink, - {0xff, 0x80, 0xab, 0xff}: Pink, - {0xff, 0x40, 0x81, 0xff}: Pink, - {0xf5, 0x00, 0x57, 0xff}: Pink, - {0xf0, 0x62, 0x92, 0xff}: Pink, - {0xec, 0x40, 0x7a, 0xff}: Pink, - {0xe9, 0x1e, 0x63, 0xff}: Pink, - {0xd8, 0x1b, 0x60, 0xff}: Pink, - {0xc2, 0x18, 0x5b, 0xff}: Pink, - {0xff, 0x00, 0xff, 0xff}: Magenta, - {0xe5, 0x00, 0xe5, 0xff}: Magenta, - {0xf0, 0x00, 0xb5, 0xff}: Magenta, - {0xce, 0x00, 0x9b, 0xff}: Magenta, - {0xc0, 0x05, 0x5b, 0xff}: Magenta, - {0xb0, 0x00, 0x85, 0xff}: Magenta, - {0xa8, 0x28, 0x63, 0xff}: Magenta, - {0x5b, 0x00, 0x2f, 0xff}: Magenta, - {0x4b, 0x01, 0x21, 0xff}: Magenta, - {0x86, 0x02, 0x25, 0xff}: Magenta, - {0xcb, 0x02, 0x3d, 0xff}: Magenta, - {0x64, 0x07, 0x1a, 0xff}: Magenta, - {0x9e, 0x00, 0x47, 0xff}: Magenta, - {0xdc, 0x7a, 0xcf, 0xff}: Magenta, - {0xed, 0xde, 0xac, 0xff}: Gold, - {0xe8, 0xb4, 0x51, 0xff}: Gold, - {0xc0, 0x8a, 0x3e, 0xff}: Gold, - {0xa2, 0x7d, 0x4b, 0xff}: Gold, - {0x75, 0x55, 0x31, 0xff}: Gold, - {0xd1, 0x93, 0x27, 0xff}: Gold, - {0xde, 0xa2, 0x53, 0xff}: Gold, - {0xd5, 0xaa, 0x6f, 0xff}: Gold, - {0xf5, 0xea, 0xd4, 0xff}: Gold, -} - -func ColorfulToIndexedColor(actualColor colorful.Color) (result IndexedColor) { - var distance = 1.0 - - for rgba, i := range IndexedColorMap { - colorColorful, _ := colorful.MakeColor(rgba) - currentDistance := colorColorful.DistanceLab(actualColor) - - if distance >= currentDistance { - distance = currentDistance - result = i - } - } - - return result -} - // Colors returns color information for a media file. -func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err error) { +func (m *MediaFile) Colors(thumbPath string) (perception colors.ColorPerception, err error) { if !m.IsJpeg() { return perception, errors.New("no color information: not a JPEG file") } @@ -426,20 +28,20 @@ func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err er pixels := float64(width * height) chromaSum := 0.0 - colorCount := make(map[IndexedColor]uint16) + colorCount := make(map[colors.Color]uint16) var mainColorCount uint16 for y := 0; y < height; y++ { for x := 0; x < width; x++ { r, g, b, a := img.At(x, y).RGBA() rgb, _ := colorful.MakeColor(color.RGBA{R: uint8(r), G: uint8(g), B: uint8(b), A: uint8(a)}) - i := ColorfulToIndexedColor(rgb) + i := colors.Colorful(rgb) perception.Colors = append(perception.Colors, i) if _, ok := colorCount[i]; ok == true { - colorCount[i] += IndexedColorWeight[i] + colorCount[i] += colors.Weights[i] } else { - colorCount[i] = IndexedColorWeight[i] + colorCount[i] = colors.Weights[i] } if colorCount[i] > mainColorCount { @@ -451,11 +53,11 @@ func (m *MediaFile) Colors(thumbPath string) (perception ColorPerception, err er chromaSum += c - perception.Luminance = append(perception.Luminance, Luminance(math.Round(l*15))) + perception.Luminance = append(perception.Luminance, colors.Luminance(math.Round(l*15))) } } - perception.Chroma = Chroma(math.Round((chromaSum / pixels) * 100)) + perception.Chroma = colors.Chroma(math.Round((chromaSum / pixels) * 100)) return perception, nil } diff --git a/internal/photoprism/colors_test.go b/internal/photoprism/colors_test.go index 077b019b5..452534999 100644 --- a/internal/photoprism/colors_test.go +++ b/internal/photoprism/colors_test.go @@ -8,6 +8,7 @@ import ( "github.com/photoprism/photoprism/internal/config" "github.com/stretchr/testify/assert" + "github.com/photoprism/photoprism/internal/colors" ) func TestMediaFile_Colors_Testdata(t *testing.T) { @@ -20,35 +21,35 @@ func TestMediaFile_Colors_Testdata(t *testing.T) { /* TODO: Add and compare other images in "testdata/" */ - expected := map[string]ColorPerception{ + expected := map[string]colors.ColorPerception{ "elephant_mono.jpg": { - Colors: IndexedColors{0x2, 0x2, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0}, + Colors: colors.Colors{0x2, 0x2, 0x0, 0x0, 0x2, 0x0, 0x0, 0x0, 0x0}, MainColor: 0, - Luminance: LightMap{0xa, 0x9, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0}, + Luminance: colors.LightMap{0xa, 0x9, 0x0, 0x0, 0x6, 0x0, 0x0, 0x0, 0x0}, Chroma: 0, }, "sharks_blue.jpg": { - Colors: IndexedColors{0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x4, 0x4, 0x6}, + Colors: colors.Colors{0x6, 0x6, 0x6, 0x6, 0x6, 0x6, 0x4, 0x4, 0x6}, MainColor: 6, - Luminance: LightMap{0x9, 0x7, 0x5, 0x4, 0x3, 0x4, 0x3, 0x3, 0x3}, + Luminance: colors.LightMap{0x9, 0x7, 0x5, 0x4, 0x3, 0x4, 0x3, 0x3, 0x3}, Chroma: 89, }, "cat_black.jpg": { - Colors: IndexedColors{0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2, 0x5, 0x2}, + Colors: colors.Colors{0x2, 0x1, 0x1, 0x1, 0x2, 0x1, 0x2, 0x5, 0x2}, MainColor: 1, - Luminance: LightMap{0x8, 0xc, 0x9, 0x4, 0x2, 0x7, 0xd, 0xd, 0x3}, + Luminance: colors.LightMap{0x8, 0xc, 0x9, 0x4, 0x2, 0x7, 0xd, 0xd, 0x3}, Chroma: 9, }, "cat_brown.jpg": { - Colors: IndexedColors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, + Colors: colors.Colors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, MainColor: 5, - Luminance: LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, + Luminance: colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, Chroma: 13, }, "cat_yellow_grey.jpg": { - Colors: IndexedColors{0x2, 0x1, 0x1, 0x9, 0x0, 0x5, 0xb, 0x0, 0x5}, + Colors: colors.Colors{0x2, 0x1, 0x1, 0x9, 0x0, 0x5, 0xb, 0x0, 0x5}, MainColor: 5, - Luminance: LightMap{0x9, 0x5, 0xb, 0x6, 0x1, 0x6, 0xa, 0x1, 0x8}, + Luminance: colors.LightMap{0x9, 0x5, 0xb, 0x6, 0x1, 0x6, 0xa, 0x1, 0x8}, Chroma: 20, }, } @@ -105,10 +106,10 @@ func TestMediaFile_Colors(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 13, p.Chroma.Int()) assert.Equal(t, "D", p.Chroma.Hex()) - assert.IsType(t, IndexedColors{}, p.Colors) + assert.IsType(t, colors.Colors{}, p.Colors) assert.Equal(t, "gold", p.MainColor.Name()) - assert.Equal(t, IndexedColors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, p.Colors) - assert.Equal(t, LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, p.Luminance) + assert.Equal(t, colors.Colors{0x9, 0x5, 0x1, 0x2, 0x2, 0x1, 0x0, 0x6, 0x2}, p.Colors) + assert.Equal(t, colors.LightMap{0x4, 0x5, 0xb, 0x4, 0x7, 0x3, 0x2, 0x5, 0x7}, p.Luminance) } else { t.Error(err) } @@ -123,10 +124,10 @@ func TestMediaFile_Colors(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 51, p.Chroma.Int()) assert.Equal(t, "33", p.Chroma.Hex()) - assert.IsType(t, IndexedColors{}, p.Colors) + assert.IsType(t, colors.Colors{}, p.Colors) assert.Equal(t, "lime", p.MainColor.Name()) - assert.Equal(t, IndexedColors{0xa, 0x9, 0xa, 0x9, 0xa, 0xa, 0x9, 0x9, 0x9}, p.Colors) - assert.Equal(t, LightMap{0xb, 0x4, 0xa, 0x6, 0x9, 0x8, 0x2, 0x3, 0x4}, p.Luminance) + assert.Equal(t, colors.Colors{0xa, 0x9, 0xa, 0x9, 0xa, 0xa, 0x9, 0x9, 0x9}, p.Colors) + assert.Equal(t, colors.LightMap{0xb, 0x4, 0xa, 0x6, 0x9, 0x8, 0x2, 0x3, 0x4}, p.Luminance) } else { t.Error(err) } @@ -141,9 +142,9 @@ func TestMediaFile_Colors(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 7, p.Chroma.Int()) assert.Equal(t, "7", p.Chroma.Hex()) - assert.IsType(t, IndexedColors{}, p.Colors) + assert.IsType(t, colors.Colors{}, p.Colors) assert.Equal(t, "blue", p.MainColor.Name()) - assert.Equal(t, IndexedColors{0x2, 0x6, 0x6, 0x2, 0x2, 0x9, 0x2, 0x0, 0x0}, p.Colors) + assert.Equal(t, colors.Colors{0x2, 0x6, 0x6, 0x2, 0x2, 0x9, 0x2, 0x0, 0x0}, p.Colors) } else { t.Error(err) } @@ -158,10 +159,10 @@ func TestMediaFile_Colors(t *testing.T) { assert.Nil(t, err) assert.Equal(t, 16, p.Chroma.Int()) assert.Equal(t, "10", p.Chroma.Hex()) - assert.IsType(t, IndexedColors{}, p.Colors) + assert.IsType(t, colors.Colors{}, p.Colors) assert.Equal(t, "gold", p.MainColor.Name()) - assert.Equal(t, IndexedColors{0x0, 0x0, 0x1, 0x5, 0x5, 0x0, 0x1, 0x5, 0x0}, p.Colors) + assert.Equal(t, colors.Colors{0x0, 0x0, 0x1, 0x5, 0x5, 0x0, 0x1, 0x5, 0x0}, p.Colors) } else { t.Error(err) } diff --git a/internal/photoprism/indexer_mediafile.go b/internal/photoprism/indexer_mediafile.go index 1ec12c1f8..90aef116f 100644 --- a/internal/photoprism/indexer_mediafile.go +++ b/internal/photoprism/indexer_mediafile.go @@ -158,6 +158,9 @@ func (i *Indexer) indexMediaFile(m *MediaFile, o IndexerOptions) IndexResult { } } + photo.PhotoYear = photo.TakenAt.Year() + photo.PhotoMonth = int(photo.TakenAt.Month()) + if photoExists { // Estimate location if o.UpdateLocation && photo.NoLocation() { @@ -369,21 +372,27 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label return keywords, labels } + if location.Place.New { + event.Publish("count.places", event.Data{ + "count": 1, + }) + } + photo.Location = location photo.LocationID = location.ID + photo.Place = location.Place + photo.PlaceID = location.PlaceID photo.LocationEstimated = false - photo.Country = entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(i.db) + country := entity.NewCountry(location.CountryCode(), location.CountryName()).FirstOrCreate(i.db) - if photo.Country.New { + if country.New { event.Publish("count.countries", event.Data{ "count": 1, }) } - countryName := photo.Country.CountryName locCategory := location.Category() - keywords = append(keywords, location.Keywords()...) // Append category from reverse location lookup @@ -395,10 +404,10 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label if (fileChanged || o.UpdateTitle) && photo.PhotoTitleChanged == false { if title := labels.Title(location.Name()); title != "" { // TODO: User defined title format log.Infof("index: using label \"%s\" to create photo title", title) - if location.LocCity == "" || len(location.LocCity) > 16 || strings.Contains(title, location.LocCity) { - photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), countryName, photo.TakenAt.Format("2006")) + if location.NoCity() || location.LongCity() || location.CityContains(title) { + photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.CountryName(), photo.TakenAt.Format("2006")) } else { - photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.LocCity, photo.TakenAt.Format("2006")) + photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", util.Title(title), location.City(), photo.TakenAt.Format("2006")) } } else if location.Name() != "" && location.City() != "" { if len(location.Name()) > 45 { @@ -406,13 +415,13 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label } else if len(location.Name()) > 20 || len(location.City()) > 16 || strings.Contains(location.Name(), location.City()) { photo.PhotoTitle = fmt.Sprintf("%s / %s", location.Name(), photo.TakenAt.Format("2006")) } else { - photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Name(), location.LocCity, photo.TakenAt.Format("2006")) + photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.Name(), location.City(), photo.TakenAt.Format("2006")) } - } else if location.City() != "" && countryName != "" { + } else if location.City() != "" && location.CountryName() != "" { if len(location.City()) > 20 { - photo.PhotoTitle = fmt.Sprintf("%s / %s", location.LocCity, photo.TakenAt.Format("2006")) + photo.PhotoTitle = fmt.Sprintf("%s / %s", location.City(), photo.TakenAt.Format("2006")) } else { - photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.LocCity, countryName, photo.TakenAt.Format("2006")) + photo.PhotoTitle = fmt.Sprintf("%s / %s / %s", location.City(), location.CountryName(), photo.TakenAt.Format("2006")) } } @@ -424,19 +433,24 @@ func (i *Indexer) indexLocation(mediaFile *MediaFile, photo *entity.Photo, label } } else { log.Debugf("index: location cannot be determined precisely (%s)", err.Error()) + photo.Place = entity.UnknownPlace + photo.PlaceID = entity.UnknownPlace.ID } + photo.PhotoCountry = photo.Place.LocCountry + return keywords, labels } func (i *Indexer) estimateLocation(photo *entity.Photo) { var recentPhoto entity.Photo - if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Country").First(&recentPhoto); result.Error == nil { - if recentPhoto.Country != nil { - photo.Country = recentPhoto.Country + if result := i.db.Unscoped().Order(gorm.Expr("ABS(DATEDIFF(taken_at, ?)) ASC", photo.TakenAt)).Preload("Place").First(&recentPhoto); result.Error == nil { + if recentPhoto.HasPlace() { + photo.Place = recentPhoto.Place + photo.PhotoCountry = photo.Place.LocCountry photo.LocationEstimated = true - log.Debugf("index: approximate location is \"%s\"", recentPhoto.Country.CountryName) + log.Debugf("index: approximate location is \"%s\"", recentPhoto.Place.Label()) } } } diff --git a/internal/photoprism/location_test.go b/internal/photoprism/location_test.go index 41641753e..cef38846a 100644 --- a/internal/photoprism/location_test.go +++ b/internal/photoprism/location_test.go @@ -43,8 +43,8 @@ func TestMediaFile_Location(t *testing.T) { t.Fatal(err) } - assert.Equal(t, "Himeji", location2.LocCity) - assert.Equal(t, "Kinki Region", location2.LocState) + assert.Equal(t, "Himeji", location2.City()) + assert.Equal(t, "Kinki Region", location2.State()) }) t.Run("cat_brown.jpg", func(t *testing.T) { conf := config.TestConfig() diff --git a/internal/repo/category.go b/internal/repo/category.go new file mode 100644 index 000000000..25ae24b54 --- /dev/null +++ b/internal/repo/category.go @@ -0,0 +1,33 @@ +package repo + +import ( + "strings" +) + +type CategoryLabel struct { + Name string + Title string +} + +func (s *Repo) CategoryLabels(limit, offset int) (results []CategoryLabel) { + q := s.db.NewScope(nil).DB() + + // q.LogMode(true) + + q = q.Table("categories"). + Select("label_name AS name"). + Joins("JOIN labels l ON categories.category_id = l.id"). + Group("label_name"). + Limit(limit).Offset(offset) + + if err := q.Scan(&results).Error; err != nil { + log.Errorf("categories: %s", err.Error()) + return results + } + + for i, l := range results { + results[i].Title = strings.Title(l.Name) + } + + return results +} diff --git a/internal/repo/category_test.go b/internal/repo/category_test.go new file mode 100644 index 000000000..ade010231 --- /dev/null +++ b/internal/repo/category_test.go @@ -0,0 +1,17 @@ +package repo + +import ( + "testing" + + "github.com/photoprism/photoprism/internal/config" +) + +func TestRepo_CategoryLabels(t *testing.T) { + conf := config.TestConfig() + + search := New(conf.OriginalsPath(), conf.Db()) + + categories := search.CategoryLabels(1000, 0) + + t.Logf("categories: %+v", categories) +} diff --git a/internal/repo/photo.go b/internal/repo/photo.go index 6847112c4..6e8d596f5 100644 --- a/internal/repo/photo.go +++ b/internal/repo/photo.go @@ -26,6 +26,9 @@ type PhotoResult struct { PhotoName string PhotoTitle string PhotoDescription string + PhotoYear int + PhotoMonth int + PhotoCountry string PhotoArtist string PhotoKeywords string PhotoColors string @@ -52,18 +55,13 @@ type PhotoResult struct { LensModel string LensMake string - // Country - CountryID string - // Location LocationID uint64 - LocName string - LocPlace string + PlaceID uint64 + LocLabel string LocCity string - LocSuburb string LocState string - LocCategory string - LocSource string + LocCountry string LocationChanged bool LocationEstimated bool @@ -118,12 +116,12 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) { files.file_orientation, files.file_main_color, files.file_colors, files.file_luminance, files.file_chroma, cameras.camera_make, cameras.camera_model, lenses.lens_make, lenses.lens_model, - locations.loc_name, locations.loc_place, locations.loc_city, locations.loc_suburb, locations.loc_state, - locations.loc_category, locations.loc_source`). + places.loc_label, places.loc_city, places.loc_state, places.loc_country + `). Joins("JOIN files ON files.photo_id = photos.id AND files.file_primary AND files.deleted_at IS NULL"). Joins("JOIN cameras ON cameras.id = photos.camera_id"). Joins("JOIN lenses ON lenses.id = photos.lens_id"). - Joins("LEFT JOIN locations ON locations.id = photos.location_id"). + Joins("JOIN places ON photos.place_id = places.id"). Joins("LEFT JOIN photos_labels ON photos_labels.photo_id = photos.id"). Where("photos.deleted_at IS NULL AND files.file_missing = 0"). Group("photos.id, files.id") @@ -220,7 +218,7 @@ func (s *Repo) Photos(f form.PhotoSearch) (results []PhotoResult, err error) { } if f.Country != "" { - q = q.Where("photos.country_id = ?", f.Country) + q = q.Where("photos.photo_country = ?", f.Country) } if f.Title != "" { diff --git a/internal/util/date.go b/internal/util/date.go new file mode 100644 index 000000000..2737d2382 --- /dev/null +++ b/internal/util/date.go @@ -0,0 +1,17 @@ +package util + +var Months = [...]string{ + "Unknown", + "January", + "February", + "March", + "April", + "May", + "June", + "July", + "August", + "September", + "October", + "November", + "December", +} diff --git a/internal/util/date_test.go b/internal/util/date_test.go new file mode 100644 index 000000000..65d61cefd --- /dev/null +++ b/internal/util/date_test.go @@ -0,0 +1,13 @@ +package util + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMonths(t *testing.T) { + assert.Equal(t, "Unknown", Months[0]) + assert.Equal(t, "January", Months[1]) + assert.Equal(t, "December", Months[12]) +}