mirror of
https://github.com/Alexander-D-Karpov/about.git
synced 2026-03-16 22:06:08 +03:00
388 lines
9.9 KiB
Go
388 lines
9.9 KiB
Go
package plugins
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/Alexander-D-Karpov/about/internal/storage"
|
|
"github.com/Alexander-D-Karpov/about/internal/stream"
|
|
)
|
|
|
|
type PlacesPlugin struct {
|
|
storage *storage.Storage
|
|
hub *stream.Hub
|
|
places []Place
|
|
lastUpdate time.Time
|
|
mutex sync.RWMutex
|
|
}
|
|
|
|
type Place struct {
|
|
Name string `json:"name"`
|
|
Lat float64 `json:"lat"`
|
|
Lng float64 `json:"lng"`
|
|
Country string `json:"country"`
|
|
City string `json:"city"`
|
|
Description string `json:"description"`
|
|
VisitedDate string `json:"visited_date"`
|
|
Photos []string `json:"photos"`
|
|
Category string `json:"category"`
|
|
}
|
|
|
|
type PlaceStats struct {
|
|
TotalPlaces int `json:"total_places"`
|
|
Countries int `json:"countries"`
|
|
Cities int `json:"cities"`
|
|
CountryList []string `json:"country_list"`
|
|
CategoryCount map[string]int `json:"category_count"`
|
|
}
|
|
|
|
func NewPlacesPlugin(storage *storage.Storage, hub *stream.Hub) *PlacesPlugin {
|
|
return &PlacesPlugin{
|
|
storage: storage,
|
|
hub: hub,
|
|
places: []Place{},
|
|
}
|
|
}
|
|
|
|
func (p *PlacesPlugin) Name() string {
|
|
return "places"
|
|
}
|
|
|
|
func (p *PlacesPlugin) Render(ctx context.Context) (string, error) {
|
|
select {
|
|
case <-ctx.Done():
|
|
return "", ctx.Err()
|
|
default:
|
|
}
|
|
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
settings := config.Settings
|
|
|
|
sectionTitle := p.getConfigValue(settings, "ui.sectionTitle", "Visited Places")
|
|
defaultZoom := p.getConfigInt(settings, "ui.defaultZoom", 2)
|
|
defaultLat := p.getConfigFloat(settings, "ui.defaultLat", 25.0)
|
|
defaultLng := p.getConfigFloat(settings, "ui.defaultLng", 0.0)
|
|
heatmapIntensity := p.getConfigFloat(settings, "ui.heatmapIntensity", 0.6)
|
|
heatmapRadius := p.getConfigInt(settings, "ui.heatmapRadius", 25)
|
|
markerRadius := p.getConfigInt(settings, "ui.markerRadius", 8)
|
|
|
|
p.mutex.RLock()
|
|
places := p.places
|
|
p.mutex.RUnlock()
|
|
|
|
if len(places) == 0 {
|
|
placesRaw, ok := settings["places"].([]interface{})
|
|
if ok {
|
|
for _, placeRaw := range placesRaw {
|
|
placeMap, ok := placeRaw.(map[string]interface{})
|
|
if !ok {
|
|
continue
|
|
}
|
|
place := Place{
|
|
Name: p.getStringFromMap(placeMap, "name", ""),
|
|
Lat: p.getFloatFromMap(placeMap, "lat", 0),
|
|
Lng: p.getFloatFromMap(placeMap, "lng", 0),
|
|
Country: p.getStringFromMap(placeMap, "country", ""),
|
|
City: p.getStringFromMap(placeMap, "city", ""),
|
|
Description: p.getStringFromMap(placeMap, "description", ""),
|
|
VisitedDate: p.getStringFromMap(placeMap, "visited_date", ""),
|
|
Category: p.getStringFromMap(placeMap, "category", "travel"),
|
|
}
|
|
if place.Lat != 0 && place.Lng != 0 {
|
|
places = append(places, place)
|
|
}
|
|
}
|
|
p.mutex.Lock()
|
|
p.places = places
|
|
p.mutex.Unlock()
|
|
}
|
|
}
|
|
|
|
placesJSON, err := json.Marshal(places)
|
|
if err != nil {
|
|
placesJSON = []byte("[]")
|
|
}
|
|
|
|
tmpl := `
|
|
<section class="places-section section plugin" id="places-plugin" data-w="2">
|
|
<header class="plugin-header">
|
|
<h3 class="plugin-title">{{.SectionTitle}}</h3>
|
|
</header>
|
|
|
|
<div class="plugin__inner">
|
|
<div class="places-map-container" id="places-map-container">
|
|
<div class="places-map" id="places-map"></div>
|
|
<div class="map-controls">
|
|
<button class="map-control-btn" id="toggle-heatmap" title="Toggle heatmap/markers">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/>
|
|
</svg>
|
|
</button>
|
|
<button class="map-control-btn" id="fit-bounds" title="Fit all places">
|
|
<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor">
|
|
<path d="M15 3l2.3 2.3-2.89 2.87 1.42 1.42L18.7 6.7 21 9V3h-6zM3 9l2.3-2.3 2.87 2.89 1.42-1.42L6.7 5.3 9 3H3v6zm6 12l-2.3-2.3 2.89-2.87-1.42-1.42L5.3 17.3 3 15v6h6zm12-6l-2.3 2.3-2.87-2.89-1.42 1.42 2.89 2.87L15 21h6v-6z"/>
|
|
</svg>
|
|
</button>
|
|
</div>
|
|
<div class="map-loading" id="map-loading">
|
|
<div class="loading"></div>
|
|
<span>Loading map...</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script id="places-data" type="application/json">{{.PlacesJSON}}</script>
|
|
<script id="places-config" type="application/json">{
|
|
"defaultZoom": {{.DefaultZoom}},
|
|
"defaultLat": {{.DefaultLat}},
|
|
"defaultLng": {{.DefaultLng}},
|
|
"heatmapIntensity": {{.HeatmapIntensity}},
|
|
"heatmapRadius": {{.HeatmapRadius}},
|
|
"markerRadius": {{.MarkerRadius}}
|
|
}</script>
|
|
</section>`
|
|
|
|
data := struct {
|
|
SectionTitle string
|
|
PlacesJSON template.JS
|
|
DefaultZoom int
|
|
DefaultLat float64
|
|
DefaultLng float64
|
|
HeatmapIntensity float64
|
|
HeatmapRadius int
|
|
MarkerRadius int
|
|
}{
|
|
SectionTitle: sectionTitle,
|
|
PlacesJSON: template.JS(placesJSON),
|
|
DefaultZoom: defaultZoom,
|
|
DefaultLat: defaultLat,
|
|
DefaultLng: defaultLng,
|
|
HeatmapIntensity: heatmapIntensity,
|
|
HeatmapRadius: heatmapRadius,
|
|
MarkerRadius: markerRadius,
|
|
}
|
|
|
|
t, err := template.New("places").Parse(tmpl)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
var buf strings.Builder
|
|
err = t.Execute(&buf, data)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return buf.String(), nil
|
|
}
|
|
|
|
func (p *PlacesPlugin) calculateStats(places []Place) PlaceStats {
|
|
stats := PlaceStats{
|
|
TotalPlaces: len(places),
|
|
CategoryCount: make(map[string]int),
|
|
}
|
|
|
|
countriesMap := make(map[string]bool)
|
|
citiesMap := make(map[string]bool)
|
|
|
|
for _, place := range places {
|
|
if place.Country != "" {
|
|
countriesMap[place.Country] = true
|
|
}
|
|
if place.City != "" {
|
|
citiesMap[place.City] = true
|
|
}
|
|
if place.Category != "" {
|
|
stats.CategoryCount[place.Category]++
|
|
}
|
|
}
|
|
|
|
stats.Countries = len(countriesMap)
|
|
stats.Cities = len(citiesMap)
|
|
|
|
for country := range countriesMap {
|
|
stats.CountryList = append(stats.CountryList, country)
|
|
}
|
|
|
|
return stats
|
|
}
|
|
|
|
func (p *PlacesPlugin) UpdateData(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (p *PlacesPlugin) GetSettings() map[string]interface{} {
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
return config.Settings
|
|
}
|
|
|
|
func (p *PlacesPlugin) SetSettings(settings map[string]interface{}) error {
|
|
config := p.storage.GetPluginConfig(p.Name())
|
|
config.Settings = settings
|
|
|
|
p.mutex.Lock()
|
|
p.places = []Place{}
|
|
p.mutex.Unlock()
|
|
|
|
err := p.storage.SetPluginConfig(p.Name(), config)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
p.hub.Broadcast("plugin_update", map[string]interface{}{
|
|
"plugin": p.Name(),
|
|
"action": "settings_changed",
|
|
})
|
|
|
|
return nil
|
|
}
|
|
|
|
func (p *PlacesPlugin) RenderText(ctx context.Context) (string, error) {
|
|
p.mutex.RLock()
|
|
places := p.places
|
|
p.mutex.RUnlock()
|
|
|
|
if len(places) == 0 {
|
|
return "Places: No places configured", nil
|
|
}
|
|
|
|
stats := p.calculateStats(places)
|
|
return fmt.Sprintf("Places: %d places in %d countries", stats.TotalPlaces, stats.Countries), nil
|
|
}
|
|
|
|
func (p *PlacesPlugin) getConfigValue(settings map[string]interface{}, key string, defaultValue string) string {
|
|
keys := strings.Split(key, ".")
|
|
current := settings
|
|
|
|
for i, k := range keys {
|
|
if i == len(keys)-1 {
|
|
if value, ok := current[k].(string); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
if next, ok := current[k].(map[string]interface{}); ok {
|
|
current = next
|
|
} else {
|
|
return defaultValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) getConfigBool(settings map[string]interface{}, key string, defaultValue bool) bool {
|
|
keys := strings.Split(key, ".")
|
|
current := settings
|
|
|
|
for i, k := range keys {
|
|
if i == len(keys)-1 {
|
|
if value, ok := current[k].(bool); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
if next, ok := current[k].(map[string]interface{}); ok {
|
|
current = next
|
|
} else {
|
|
return defaultValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) getConfigInt(settings map[string]interface{}, key string, defaultValue int) int {
|
|
keys := strings.Split(key, ".")
|
|
current := settings
|
|
|
|
for i, k := range keys {
|
|
if i == len(keys)-1 {
|
|
if value, ok := current[k].(float64); ok {
|
|
return int(value)
|
|
}
|
|
if value, ok := current[k].(int); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
if next, ok := current[k].(map[string]interface{}); ok {
|
|
current = next
|
|
} else {
|
|
return defaultValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) getConfigFloat(settings map[string]interface{}, key string, defaultValue float64) float64 {
|
|
keys := strings.Split(key, ".")
|
|
current := settings
|
|
|
|
for i, k := range keys {
|
|
if i == len(keys)-1 {
|
|
if value, ok := current[k].(float64); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
if next, ok := current[k].(map[string]interface{}); ok {
|
|
current = next
|
|
} else {
|
|
return defaultValue
|
|
}
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) getStringFromMap(m map[string]interface{}, key string, defaultValue string) string {
|
|
if value, ok := m[key].(string); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) getFloatFromMap(m map[string]interface{}, key string, defaultValue float64) float64 {
|
|
if value, ok := m[key].(float64); ok {
|
|
return value
|
|
}
|
|
return defaultValue
|
|
}
|
|
|
|
func (p *PlacesPlugin) GetMetrics() map[string]interface{} {
|
|
p.mutex.RLock()
|
|
defer p.mutex.RUnlock()
|
|
|
|
metrics := map[string]interface{}{
|
|
"total_places": 0,
|
|
"countries_count": 0,
|
|
"cities_count": 0,
|
|
}
|
|
|
|
if len(p.places) == 0 {
|
|
return metrics
|
|
}
|
|
|
|
metrics["total_places"] = len(p.places)
|
|
|
|
countriesMap := make(map[string]bool)
|
|
citiesMap := make(map[string]bool)
|
|
|
|
for _, place := range p.places {
|
|
if place.Country != "" {
|
|
countriesMap[place.Country] = true
|
|
}
|
|
if place.City != "" {
|
|
citiesMap[place.City] = true
|
|
}
|
|
}
|
|
|
|
metrics["countries_count"] = len(countriesMap)
|
|
metrics["cities_count"] = len(citiesMap)
|
|
|
|
return metrics
|
|
}
|