mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 17:21:38 -05:00
commit
2ecd1b7763
37 changed files with 804 additions and 57 deletions
|
|
@ -1 +1 @@
|
|||
0.24.0
|
||||
0.24.1
|
||||
|
|
|
|||
33
CHANGELOG.md
33
CHANGELOG.md
|
|
@ -4,7 +4,38 @@ All notable changes to this project will be documented in this file.
|
|||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
# 0.24.0 - 2025-02-09
|
||||
# 0.24.1 - 2025-02-13
|
||||
|
||||
## Custom map tiles
|
||||
|
||||
In the user settings, you can now set a custom tile URL for the map. This is useful if you want to use a custom map tile provider or if you want to use a map tile provider that is not listed in the dropdown.
|
||||
|
||||
To set a custom tile URL, go to the user settings and set the `Maps` section to your liking. Be mindful that currently, only raster tiles are supported. The URL should be a valid tile URL, like `https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png`. You, as the user, are responsible for any extra costs that may occur due to using a custom tile URL.
|
||||
|
||||
### Added
|
||||
|
||||
- Safe settings for user with default values.
|
||||
- Nominatim API is now supported as a reverse geocoding provider.
|
||||
- In the user settings, you can now set a custom tile URL for the map. #429 #715
|
||||
- In the user map settings, you can now see a chart of map tiles usage.
|
||||
- If you have Prometheus exporter enabled, you can now see a `ruby_dawarich_map_tiles` metric in Prometheus, which shows the total number of map tiles loaded. Example:
|
||||
|
||||
```
|
||||
# HELP ruby_dawarich_map_tiles_usage
|
||||
# TYPE ruby_dawarich_map_tiles_usage counter
|
||||
ruby_dawarich_map_tiles_usage 99
|
||||
```
|
||||
|
||||
### Fixed
|
||||
|
||||
- Speed on the Points page is now being displayed in kilometers per hour. #700
|
||||
- Fog of war displacement #774
|
||||
|
||||
### Reverted
|
||||
|
||||
- #748
|
||||
|
||||
# 0.24.0 - 2025-02-10
|
||||
|
||||
## Points speed units
|
||||
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
prometheus_exporter: bundle exec prometheus_exporter -b ANY
|
||||
web: bin/rails server -p 3000 -b ::
|
||||
web: bin/rails server -p 3000 -b ::
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ Donate using crypto: [0x6bAd13667692632f1bF926cA9B421bEe7EaEB8D4](https://ethers
|
|||
- Explore statistics like the number of countries and cities visited, total distance traveled, and more!
|
||||
|
||||
📄 **Changelog**: Find the latest updates [here](CHANGELOG.md).
|
||||
|
||||
👩💻 **Contribute**: See [CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute to Dawarich.
|
||||
---
|
||||
|
||||
|
|
|
|||
15
app/controllers/api/v1/maps/tile_usage_controller.rb
Normal file
15
app/controllers/api/v1/maps/tile_usage_controller.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Api::V1::Maps::TileUsageController < ApiController
|
||||
def create
|
||||
Maps::TileUsage::Track.new(current_api_user.id, tile_usage_params[:count].to_i).call
|
||||
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tile_usage_params
|
||||
params.require(:tile_usage).permit(:count)
|
||||
end
|
||||
end
|
||||
29
app/controllers/settings/maps_controller.rb
Normal file
29
app/controllers/settings/maps_controller.rb
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Settings::MapsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
|
||||
def index
|
||||
@maps = current_user.safe_settings.maps
|
||||
|
||||
@tile_usage = 7.days.ago.to_date.upto(Time.zone.today).map do |date|
|
||||
[
|
||||
date.to_s,
|
||||
Rails.cache.read("dawarich_map_tiles_usage:#{current_user.id}:#{date}") || 0
|
||||
]
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
current_user.settings['maps'] = settings_params
|
||||
current_user.save!
|
||||
|
||||
redirect_to settings_maps_path, notice: 'Settings updated'
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def settings_params
|
||||
params.require(:maps).permit(:name, :url)
|
||||
end
|
||||
end
|
||||
|
|
@ -120,4 +120,10 @@ module ApplicationHelper
|
|||
|
||||
'text-red-500'
|
||||
end
|
||||
|
||||
def point_speed(speed)
|
||||
return speed if speed.to_i <= 0
|
||||
|
||||
speed * 3.6
|
||||
end
|
||||
end
|
||||
|
|
|
|||
67
app/javascript/controllers/map_preview_controller.js
Normal file
67
app/javascript/controllers/map_preview_controller.js
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { Controller } from "@hotwired/stimulus"
|
||||
import L from "leaflet"
|
||||
import { showFlashMessage } from "../maps/helpers"
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["urlInput", "mapContainer", "saveButton"]
|
||||
|
||||
DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
|
||||
|
||||
connect() {
|
||||
console.log("Controller connected!")
|
||||
// Wait for the next frame to ensure the DOM is ready
|
||||
requestAnimationFrame(() => {
|
||||
// Force container height
|
||||
this.mapContainerTarget.style.height = '500px'
|
||||
this.initializeMap()
|
||||
})
|
||||
}
|
||||
|
||||
initializeMap() {
|
||||
console.log("Initializing map...")
|
||||
if (!this.map) {
|
||||
this.map = L.map(this.mapContainerTarget).setView([51.505, -0.09], 13)
|
||||
// Invalidate size after initialization
|
||||
setTimeout(() => {
|
||||
this.map.invalidateSize()
|
||||
}, 0)
|
||||
this.updatePreview()
|
||||
}
|
||||
}
|
||||
|
||||
updatePreview() {
|
||||
console.log("Updating preview...")
|
||||
const url = this.urlInputTarget.value || this.DEFAULT_TILE_URL
|
||||
|
||||
// Only animate if save button target exists
|
||||
if (this.hasSaveButtonTarget) {
|
||||
this.saveButtonTarget.classList.add('btn-animate')
|
||||
setTimeout(() => {
|
||||
this.saveButtonTarget.classList.remove('btn-animate')
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (this.currentLayer) {
|
||||
this.map.removeLayer(this.currentLayer)
|
||||
}
|
||||
|
||||
try {
|
||||
this.currentLayer = L.tileLayer(url, {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map)
|
||||
} catch (e) {
|
||||
console.error('Invalid tile URL:', e)
|
||||
showFlashMessage('error', 'Invalid tile URL. Reverting to OpenStreetMap.')
|
||||
|
||||
// Reset input to default OSM URL
|
||||
this.urlInputTarget.value = this.DEFAULT_TILE_URL
|
||||
|
||||
// Create default layer
|
||||
this.currentLayer = L.tileLayer(this.DEFAULT_TILE_URL, {
|
||||
maxZoom: 19,
|
||||
attribution: '© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||
}).addTo(this.map)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,9 +8,7 @@ import { createMarkersArray } from "../maps/markers";
|
|||
import {
|
||||
createPolylinesLayer,
|
||||
updatePolylinesOpacity,
|
||||
updatePolylinesColors,
|
||||
calculateSpeed,
|
||||
getSpeedColor
|
||||
updatePolylinesColors
|
||||
} from "../maps/polylines";
|
||||
|
||||
import { fetchAndDrawAreas, handleAreaCreated } from "../maps/areas";
|
||||
|
|
@ -32,6 +30,7 @@ import { countryCodesMap } from "../maps/country_codes";
|
|||
|
||||
import "leaflet-draw";
|
||||
import { initializeFogCanvas, drawFogCanvas, createFogOverlay } from "../maps/fog_of_war";
|
||||
import { TileMonitor } from "../maps/tile_monitor";
|
||||
|
||||
export default class extends Controller {
|
||||
static targets = ["container"];
|
||||
|
|
@ -245,6 +244,19 @@ export default class extends Controller {
|
|||
if (this.liveMapEnabled) {
|
||||
this.setupSubscription();
|
||||
}
|
||||
|
||||
// Initialize tile monitor
|
||||
this.tileMonitor = new TileMonitor(this.apiKey);
|
||||
|
||||
// Add tile load event handlers to each base layer
|
||||
Object.entries(this.baseMaps()).forEach(([name, layer]) => {
|
||||
layer.on('tileload', () => {
|
||||
this.tileMonitor.recordTileLoad(name);
|
||||
});
|
||||
});
|
||||
|
||||
// Start monitoring
|
||||
this.tileMonitor.startMonitoring();
|
||||
}
|
||||
|
||||
disconnect() {
|
||||
|
|
@ -260,6 +272,11 @@ export default class extends Controller {
|
|||
if (this.map) {
|
||||
this.map.remove();
|
||||
}
|
||||
|
||||
// Stop tile monitoring
|
||||
if (this.tileMonitor) {
|
||||
this.tileMonitor.stopMonitoring();
|
||||
}
|
||||
}
|
||||
|
||||
setupSubscription() {
|
||||
|
|
@ -385,8 +402,7 @@ export default class extends Controller {
|
|||
|
||||
baseMaps() {
|
||||
let selectedLayerName = this.userSettings.preferred_map_layer || "OpenStreetMap";
|
||||
|
||||
return {
|
||||
let maps = {
|
||||
OpenStreetMap: osmMapLayer(this.map, selectedLayerName),
|
||||
"OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName),
|
||||
OPNV: OPNVMapLayer(this.map, selectedLayerName),
|
||||
|
|
@ -397,6 +413,33 @@ export default class extends Controller {
|
|||
esriWorldImagery: esriWorldImageryMapLayer(this.map, selectedLayerName),
|
||||
esriWorldGrayCanvas: esriWorldGrayCanvasMapLayer(this.map, selectedLayerName)
|
||||
};
|
||||
|
||||
// Add custom map if it exists in settings
|
||||
if (this.userSettings.maps && this.userSettings.maps.url) {
|
||||
const customLayer = L.tileLayer(this.userSettings.maps.url, {
|
||||
maxZoom: 19,
|
||||
attribution: "© OpenStreetMap contributors"
|
||||
});
|
||||
|
||||
// If this is the preferred layer, add it to the map immediately
|
||||
if (selectedLayerName === this.userSettings.maps.name) {
|
||||
customLayer.addTo(this.map);
|
||||
// Remove any other base layers that might be active
|
||||
Object.values(maps).forEach(layer => {
|
||||
if (this.map.hasLayer(layer)) {
|
||||
this.map.removeLayer(layer);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
maps[this.userSettings.maps.name] = customLayer;
|
||||
} else {
|
||||
// If no custom map is set, ensure a default layer is added
|
||||
const defaultLayer = maps[selectedLayerName] || maps["OpenStreetMap"];
|
||||
defaultLayer.addTo(this.map);
|
||||
}
|
||||
|
||||
return maps;
|
||||
}
|
||||
|
||||
removeEventListeners() {
|
||||
|
|
|
|||
|
|
@ -85,15 +85,12 @@ export function createFogOverlay() {
|
|||
onAdd: (map) => {
|
||||
initializeFogCanvas(map);
|
||||
|
||||
// Add drag event handlers to update fog during marker movement
|
||||
map.on('drag', () => {
|
||||
const fog = document.getElementById('fog');
|
||||
if (fog) {
|
||||
// Update fog canvas position to match map position
|
||||
const mapPos = map.getContainer().getBoundingClientRect();
|
||||
fog.style.left = `${mapPos.left}px`;
|
||||
fog.style.top = `${mapPos.top}px`;
|
||||
}
|
||||
// Add resize event handlers to update fog size
|
||||
map.on('resize', () => {
|
||||
// Set canvas size to match map container
|
||||
const mapSize = map.getSize();
|
||||
fog.width = mapSize.x;
|
||||
fog.height = mapSize.y;
|
||||
});
|
||||
},
|
||||
onRemove: (map) => {
|
||||
|
|
@ -102,7 +99,7 @@ export function createFogOverlay() {
|
|||
fog.remove();
|
||||
}
|
||||
// Clean up event listener
|
||||
map.off('drag');
|
||||
map.off('resize');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
63
app/javascript/maps/tile_monitor.js
Normal file
63
app/javascript/maps/tile_monitor.js
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
export class TileMonitor {
|
||||
constructor(apiKey) {
|
||||
this.apiKey = apiKey;
|
||||
this.tileQueue = 0;
|
||||
this.tileUpdateInterval = null;
|
||||
}
|
||||
|
||||
startMonitoring() {
|
||||
// Clear any existing interval
|
||||
if (this.tileUpdateInterval) {
|
||||
clearInterval(this.tileUpdateInterval);
|
||||
}
|
||||
|
||||
// Set up a regular interval to send stats
|
||||
this.tileUpdateInterval = setInterval(() => {
|
||||
this.sendTileUsage();
|
||||
}, 5000); // Exactly every 5 seconds
|
||||
}
|
||||
|
||||
stopMonitoring() {
|
||||
if (this.tileUpdateInterval) {
|
||||
clearInterval(this.tileUpdateInterval);
|
||||
this.sendTileUsage(); // Send any remaining stats
|
||||
}
|
||||
}
|
||||
|
||||
recordTileLoad() {
|
||||
this.tileQueue += 1;
|
||||
}
|
||||
|
||||
sendTileUsage() {
|
||||
if (this.tileQueue === 0) return;
|
||||
|
||||
const currentCount = this.tileQueue;
|
||||
console.log('Sending tile usage batch:', currentCount);
|
||||
|
||||
fetch('/api/v1/maps/tile_usage', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${this.apiKey}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
tile_usage: {
|
||||
count: currentCount
|
||||
}
|
||||
})
|
||||
})
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
// Only subtract sent count if it hasn't changed
|
||||
if (this.tileQueue === currentCount) {
|
||||
this.tileQueue = 0;
|
||||
} else {
|
||||
this.tileQueue -= currentCount;
|
||||
}
|
||||
console.log('Tile usage batch sent successfully');
|
||||
})
|
||||
.catch(error => console.error('Error recording tile usage:', error));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,13 +16,18 @@ class User < ApplicationRecord
|
|||
has_many :trips, dependent: :destroy
|
||||
|
||||
after_create :create_api_key
|
||||
before_save :strip_trailing_slashes
|
||||
before_save :sanitize_input
|
||||
|
||||
validates :email, presence: true
|
||||
|
||||
validates :reset_password_token, uniqueness: true, allow_nil: true
|
||||
|
||||
attribute :admin, :boolean, default: false
|
||||
|
||||
def safe_settings
|
||||
Users::SafeSettings.new(settings)
|
||||
end
|
||||
|
||||
def countries_visited
|
||||
stats.pluck(:toponyms).flatten.map { _1['country'] }.uniq.compact
|
||||
end
|
||||
|
|
@ -99,8 +104,9 @@ class User < ApplicationRecord
|
|||
save
|
||||
end
|
||||
|
||||
def strip_trailing_slashes
|
||||
def sanitize_input
|
||||
settings['immich_url']&.gsub!(%r{/+\z}, '')
|
||||
settings['photoprism_url']&.gsub!(%r{/+\z}, '')
|
||||
settings.try(:[], 'maps')&.try(:[], 'url')&.strip!
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -6,8 +6,8 @@ class Areas::Visits::Create
|
|||
def initialize(user, areas)
|
||||
@user = user
|
||||
@areas = areas
|
||||
@time_threshold_minutes = 30 || user.settings['time_threshold_minutes']
|
||||
@merge_threshold_minutes = 15 || user.settings['merge_threshold_minutes']
|
||||
@time_threshold_minutes = 30 || user.safe_settings.time_threshold_minutes
|
||||
@merge_threshold_minutes = 15 || user.safe_settings.merge_threshold_minutes
|
||||
end
|
||||
|
||||
def call
|
||||
|
|
|
|||
|
|
@ -5,15 +5,15 @@ class Immich::RequestPhotos
|
|||
|
||||
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
||||
@user = user
|
||||
@immich_api_base_url = URI.parse("#{user.settings['immich_url']}/api/search/metadata")
|
||||
@immich_api_key = user.settings['immich_api_key']
|
||||
@immich_api_base_url = URI.parse("#{user.safe_settings.immich_url}/api/search/metadata")
|
||||
@immich_api_key = user.safe_settings.immich_api_key
|
||||
@start_date = start_date
|
||||
@end_date = end_date
|
||||
end
|
||||
|
||||
def call
|
||||
raise ArgumentError, 'Immich API key is missing' if immich_api_key.blank?
|
||||
raise ArgumentError, 'Immich URL is missing' if user.settings['immich_url'].blank?
|
||||
raise ArgumentError, 'Immich URL is missing' if user.safe_settings.immich_url.blank?
|
||||
|
||||
data = retrieve_immich_data
|
||||
|
||||
|
|
|
|||
36
app/services/maps/tile_usage/track.rb
Normal file
36
app/services/maps/tile_usage/track.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Maps::TileUsage::Track
|
||||
def initialize(user_id, count = 1)
|
||||
@user_id = user_id
|
||||
@count = count
|
||||
end
|
||||
|
||||
def call
|
||||
report_to_prometheus
|
||||
report_to_cache
|
||||
rescue StandardError => e
|
||||
Rails.logger.error("Failed to send tile usage metric: #{e.message}")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def report_to_prometheus
|
||||
return unless DawarichSettings.prometheus_exporter_enabled?
|
||||
|
||||
metric_data = {
|
||||
type: 'counter',
|
||||
name: 'dawarich_map_tiles_usage',
|
||||
value: @count
|
||||
}
|
||||
|
||||
PrometheusExporter::Client.default.send_json(metric_data)
|
||||
end
|
||||
|
||||
def report_to_cache
|
||||
today_key = "dawarich_map_tiles_usage:#{@user_id}:#{Time.zone.today}"
|
||||
|
||||
current_value = (Rails.cache.read(today_key) || 0).to_i
|
||||
Rails.cache.write(today_key, current_value + @count, expires_in: 7.days)
|
||||
end
|
||||
end
|
||||
|
|
@ -9,14 +9,14 @@ class Photoprism::RequestPhotos
|
|||
|
||||
def initialize(user, start_date: '1970-01-01', end_date: nil)
|
||||
@user = user
|
||||
@photoprism_api_base_url = URI.parse("#{user.settings['photoprism_url']}/api/v1/photos")
|
||||
@photoprism_api_key = user.settings['photoprism_api_key']
|
||||
@photoprism_api_base_url = URI.parse("#{user.safe_settings.photoprism_url}/api/v1/photos")
|
||||
@photoprism_api_key = user.safe_settings.photoprism_api_key
|
||||
@start_date = start_date
|
||||
@end_date = end_date
|
||||
end
|
||||
|
||||
def call
|
||||
raise ArgumentError, 'Photoprism URL is missing' if user.settings['photoprism_url'].blank?
|
||||
raise ArgumentError, 'Photoprism URL is missing' if user.safe_settings.photoprism_url.blank?
|
||||
raise ArgumentError, 'Photoprism API key is missing' if photoprism_api_key.blank?
|
||||
|
||||
data = retrieve_photoprism_data
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Photos::Thumbnail
|
||||
SUPPORTED_SOURCES = %w[immich photoprism].freeze
|
||||
|
||||
def initialize(user, source, id)
|
||||
@user = user
|
||||
@source = source
|
||||
|
|
@ -8,6 +10,8 @@ class Photos::Thumbnail
|
|||
end
|
||||
|
||||
def call
|
||||
raise unsupported_source_error unless SUPPORTED_SOURCES.include?(source)
|
||||
|
||||
HTTParty.get(request_url, headers: headers)
|
||||
end
|
||||
|
||||
|
|
@ -16,11 +20,11 @@ class Photos::Thumbnail
|
|||
attr_reader :user, :source, :id
|
||||
|
||||
def source_url
|
||||
user.settings["#{source}_url"]
|
||||
user.safe_settings.public_send("#{source}_url")
|
||||
end
|
||||
|
||||
def source_api_key
|
||||
user.settings["#{source}_api_key"]
|
||||
user.safe_settings.public_send("#{source}_api_key")
|
||||
end
|
||||
|
||||
def source_path
|
||||
|
|
@ -30,8 +34,6 @@ class Photos::Thumbnail
|
|||
when 'photoprism'
|
||||
preview_token = Rails.cache.read("#{Photoprism::CachePreviewToken::TOKEN_CACHE_KEY}_#{user.id}")
|
||||
"/api/v1/t/#{id}/#{preview_token}/tile_500"
|
||||
else
|
||||
raise "Unsupported source: #{source}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -48,4 +50,8 @@ class Photos::Thumbnail
|
|||
|
||||
request_headers
|
||||
end
|
||||
|
||||
def unsupported_source_error
|
||||
raise ArgumentError, "Unsupported source: #{source}"
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -96,7 +96,13 @@ class ReverseGeocoding::Places::FetchData
|
|||
end
|
||||
|
||||
def reverse_geocoded_places
|
||||
data = Geocoder.search([place.latitude, place.longitude], limit: 10, distance_sort: true)
|
||||
data = Geocoder.search(
|
||||
[place.latitude, place.longitude],
|
||||
limit: 10,
|
||||
distance_sort: true,
|
||||
radius: 1,
|
||||
units: DISTANCE_UNITS
|
||||
)
|
||||
|
||||
data.reject do |place|
|
||||
place.data['properties']['osm_value'].in?(IGNORED_OSM_VALUES) ||
|
||||
|
|
|
|||
93
app/services/users/safe_settings.rb
Normal file
93
app/services/users/safe_settings.rb
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Users::SafeSettings
|
||||
attr_reader :settings
|
||||
|
||||
def initialize(settings)
|
||||
@settings = settings
|
||||
end
|
||||
|
||||
# rubocop:disable Metrics/MethodLength
|
||||
def config
|
||||
{
|
||||
fog_of_war_meters: fog_of_war_meters,
|
||||
meters_between_routes: meters_between_routes,
|
||||
preferred_map_layer: preferred_map_layer,
|
||||
speed_colored_routes: speed_colored_routes,
|
||||
points_rendering_mode: points_rendering_mode,
|
||||
minutes_between_routes: minutes_between_routes,
|
||||
time_threshold_minutes: time_threshold_minutes,
|
||||
merge_threshold_minutes: merge_threshold_minutes,
|
||||
live_map_enabled: live_map_enabled,
|
||||
route_opacity: route_opacity,
|
||||
immich_url: immich_url,
|
||||
immich_api_key: immich_api_key,
|
||||
photoprism_url: photoprism_url,
|
||||
photoprism_api_key: photoprism_api_key,
|
||||
maps: maps
|
||||
}
|
||||
end
|
||||
# rubocop:enable Metrics/MethodLength
|
||||
|
||||
def fog_of_war_meters
|
||||
settings['fog_of_war_meters'] || 50
|
||||
end
|
||||
|
||||
def meters_between_routes
|
||||
settings['meters_between_routes'] || 500
|
||||
end
|
||||
|
||||
def preferred_map_layer
|
||||
settings['preferred_map_layer'] || 'OpenStreetMap'
|
||||
end
|
||||
|
||||
def speed_colored_routes
|
||||
settings['speed_colored_routes'] || false
|
||||
end
|
||||
|
||||
def points_rendering_mode
|
||||
settings['points_rendering_mode'] || 'raw'
|
||||
end
|
||||
|
||||
def minutes_between_routes
|
||||
settings['minutes_between_routes'] || 30
|
||||
end
|
||||
|
||||
def time_threshold_minutes
|
||||
settings['time_threshold_minutes'] || 30
|
||||
end
|
||||
|
||||
def merge_threshold_minutes
|
||||
settings['merge_threshold_minutes'] || 15
|
||||
end
|
||||
|
||||
def live_map_enabled
|
||||
return settings['live_map_enabled'] if settings.key?('live_map_enabled')
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def route_opacity
|
||||
settings['route_opacity'] || 0.6
|
||||
end
|
||||
|
||||
def immich_url
|
||||
settings['immich_url']
|
||||
end
|
||||
|
||||
def immich_api_key
|
||||
settings['immich_api_key']
|
||||
end
|
||||
|
||||
def photoprism_url
|
||||
settings['photoprism_url']
|
||||
end
|
||||
|
||||
def photoprism_api_key
|
||||
settings['photoprism_api_key']
|
||||
end
|
||||
|
||||
def maps
|
||||
settings['maps'] || {}
|
||||
end
|
||||
end
|
||||
|
|
@ -5,12 +5,12 @@
|
|||
<h1 class="font-bold text-4xl">Imports</h1>
|
||||
<%= link_to "New import", new_import_path, class: "rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium" %>
|
||||
|
||||
<% if current_user.settings['immich_url'] && current_user.settings['immich_api_key'] %>
|
||||
<% if current_user.safe_settings.immich_url && current_user.safe_settings.immich_api_key %>
|
||||
<%= link_to 'Import Immich data', settings_background_jobs_path(job_name: 'start_immich_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
||||
<% else %>
|
||||
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Immich instance data in the Settings">Import Immich data</a>
|
||||
<% end %>
|
||||
<% if current_user.settings['photoprism_url'] && current_user.settings['photoprism_api_key'] %>
|
||||
<% if current_user.safe_settings.photoprism_url && current_user.safe_settings.photoprism_api_key %>
|
||||
<%= link_to 'Import Photoprism data', settings_background_jobs_path(job_name: 'start_photoprism_import'), method: :post, data: { confirm: 'Are you sure?', turbo_confirm: 'Are you sure?', turbo_method: :post }, class: 'rounded-lg py-3 px-5 bg-blue-600 text-white block font-medium' %>
|
||||
<% else %>
|
||||
<a href='' class="rounded-lg py-3 px-5 bg-blue-900 text-gray block font-medium tooltip cursor-not-allowed" data-tip="You need to provide your Photoprism instance data in the Settings">Import Photoprism data</a>
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@
|
|||
data-points-target="map"
|
||||
data-distance_unit="<%= DISTANCE_UNIT %>"
|
||||
data-api_key="<%= current_user.api_key %>"
|
||||
data-user_settings=<%= current_user.settings.to_json %>
|
||||
data-user_settings='<%= current_user.settings.to_json.html_safe %>'
|
||||
data-coordinates="<%= @coordinates %>"
|
||||
data-distance="<%= @distance %>"
|
||||
data-points_number="<%= @points_number %>"
|
||||
|
|
|
|||
|
|
@ -4,4 +4,5 @@
|
|||
<%= link_to 'Users', settings_users_path, role: 'tab', class: "tab #{active_tab?(settings_users_path)}" %>
|
||||
<%= link_to 'Background Jobs', settings_background_jobs_path, role: 'tab', class: "tab #{active_tab?(settings_background_jobs_path)}" %>
|
||||
<% end %>
|
||||
<%= link_to 'Map', settings_maps_path, role: 'tab', class: "tab #{active_tab?(settings_maps_path)}" %>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -9,20 +9,20 @@
|
|||
<%= form_for :settings, url: settings_path, method: :patch, data: { turbo_method: :patch, turbo: false } do |f| %>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :immich_url %>
|
||||
<%= f.text_field :immich_url, value: current_user.settings['immich_url'], class: "input input-bordered", placeholder: 'http://192.168.0.1:2283' %>
|
||||
<%= f.text_field :immich_url, value: current_user.safe_settings.immich_url, class: "input input-bordered", placeholder: 'http://192.168.0.1:2283' %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :immich_api_key %>
|
||||
<%= f.text_field :immich_api_key, value: current_user.settings['immich_api_key'], class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||
<%= f.text_field :immich_api_key, value: current_user.safe_settings.immich_api_key, class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||
</div>
|
||||
<div class="divider"></div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :photoprism_url %>
|
||||
<%= f.text_field :photoprism_url, value: current_user.settings['photoprism_url'], class: "input input-bordered", placeholder: 'http://192.168.0.1:2342' %>
|
||||
<%= f.text_field :photoprism_url, value: current_user.safe_settings.photoprism_url, class: "input input-bordered", placeholder: 'http://192.168.0.1:2342' %>
|
||||
</div>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :photoprism_api_key %>
|
||||
<%= f.text_field :photoprism_api_key, value: current_user.settings['photoprism_api_key'], class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||
<%= f.text_field :photoprism_api_key, value: current_user.safe_settings.photoprism_api_key, class: "input input-bordered", placeholder: 'xxxxxxxxxxxxxx' %>
|
||||
</div>
|
||||
|
||||
<div class="form-control my-2">
|
||||
|
|
|
|||
71
app/views/settings/maps/index.html.erb
Normal file
71
app/views/settings/maps/index.html.erb
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<% content_for :title, "Background jobs" %>
|
||||
|
||||
<div class="min-h-content w-full my-5">
|
||||
<%= render 'settings/navigation' %>
|
||||
|
||||
<div class="flex justify-between items-center my-5">
|
||||
<h1 class="font-bold text-4xl">Maps settings</h1>
|
||||
</div>
|
||||
|
||||
<div role="alert" class="alert alert-info">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
class="h-6 w-6 shrink-0 stroke-current">
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
|
||||
</svg>
|
||||
<span>Please remember, that using a custom tile URL may result in extra costs. Check your map tile provider's terms of service for more information.</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-4 mt-5" data-controller="map-preview">
|
||||
<div class="flex flex-col gap-4">
|
||||
<%= form_for :maps,
|
||||
url: settings_maps_path,
|
||||
method: :patch,
|
||||
autocomplete: "off",
|
||||
data: { turbo_method: :patch, turbo: false } do |f| %>
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :name %>
|
||||
<%= f.text_field :name, value: @maps['name'], placeholder: 'Example: OpenStreetMap', class: "input input-bordered" %>
|
||||
</div>
|
||||
|
||||
<div class="form-control my-2">
|
||||
<%= f.label :url, 'URL' %>
|
||||
<%= f.text_field :url,
|
||||
value: @maps['url'],
|
||||
autocomplete: "off",
|
||||
placeholder: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||||
class: "input input-bordered",
|
||||
data: {
|
||||
map_preview_target: "urlInput",
|
||||
action: "input->map-preview#updatePreview"
|
||||
} %>
|
||||
</div>
|
||||
|
||||
<%= f.submit 'Save', class: "btn btn-primary", data: { map_preview_target: "saveButton" } %>
|
||||
<% end %>
|
||||
|
||||
<h2 class="text-lg font-bold">Tile usage</h2>
|
||||
|
||||
<%= line_chart(
|
||||
@tile_usage,
|
||||
height: '200px',
|
||||
xtitle: 'Days',
|
||||
ytitle: 'Tiles',
|
||||
suffix: ' tiles loaded'
|
||||
) %>
|
||||
</div>
|
||||
|
||||
<div style="height: 500px;">
|
||||
<div
|
||||
data-map-preview-target="mapContainer"
|
||||
class="w-full h-full rounded-lg border"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -13,5 +13,9 @@ PHOTON_API_HOST = ENV.fetch('PHOTON_API_HOST', nil)
|
|||
PHOTON_API_KEY = ENV.fetch('PHOTON_API_KEY', nil)
|
||||
PHOTON_API_USE_HTTPS = ENV.fetch('PHOTON_API_USE_HTTPS', 'true') == 'true'
|
||||
|
||||
NOMINATIM_API_HOST = ENV.fetch('NOMINATIM_API_HOST', nil)
|
||||
NOMINATIM_API_KEY = ENV.fetch('NOMINATIM_API_KEY', nil)
|
||||
NOMINATIM_API_USE_HTTPS = ENV.fetch('NOMINATIM_API_USE_HTTPS', 'true') == 'true'
|
||||
|
||||
GEOAPIFY_API_KEY = ENV.fetch('GEOAPIFY_API_KEY', nil)
|
||||
# /Reverse geocoding settings
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
class DawarichSettings
|
||||
class << self
|
||||
def reverse_geocoding_enabled?
|
||||
@reverse_geocoding_enabled ||= photon_enabled? || geoapify_enabled?
|
||||
@reverse_geocoding_enabled ||= photon_enabled? || geoapify_enabled? || nominatim_enabled?
|
||||
end
|
||||
|
||||
def photon_enabled?
|
||||
|
|
@ -18,12 +18,15 @@ class DawarichSettings
|
|||
@geoapify_enabled ||= GEOAPIFY_API_KEY.present?
|
||||
end
|
||||
|
||||
def meters_between_tracks
|
||||
@meters_between_tracks ||= 300
|
||||
def prometheus_exporter_enabled?
|
||||
@prometheus_exporter_enabled ||=
|
||||
ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true' &&
|
||||
ENV['PROMETHEUS_EXPORTER_HOST'].present? &&
|
||||
ENV['PROMETHEUS_EXPORTER_PORT'].present?
|
||||
end
|
||||
|
||||
def minutes_between_tracks
|
||||
@minutes_between_tracks ||= 20
|
||||
def nominatim_enabled?
|
||||
@nominatim_enabled ||= NOMINATIM_API_HOST.present?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ if PHOTON_API_HOST.present?
|
|||
elsif GEOAPIFY_API_KEY.present?
|
||||
settings[:lookup] = :geoapify
|
||||
settings[:api_key] = GEOAPIFY_API_KEY
|
||||
elsif NOMINATIM_API_HOST.present?
|
||||
settings[:lookup] = :nominatim
|
||||
settings[:nominatim] = { use_https: NOMINATIM_API_USE_HTTPS, host: NOMINATIM_API_HOST }
|
||||
if NOMINATIM_API_KEY.present?
|
||||
settings[:api_key] = NOMINATIM_API_KEY
|
||||
end
|
||||
end
|
||||
|
||||
Geocoder.configure(settings)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
if !Rails.env.test? && ENV['PROMETHEUS_EXPORTER_ENABLED'].to_s == 'true'
|
||||
if !Rails.env.test? && DawarichSettings.prometheus_exporter_enabled?
|
||||
require 'prometheus_exporter/middleware'
|
||||
require 'prometheus_exporter/instrumentation'
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +0,0 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Reddis
|
||||
def self.client
|
||||
@client ||= Redis.new(url: ENV['REDIS_URL'])
|
||||
end
|
||||
end
|
||||
|
|
@ -20,6 +20,8 @@ Rails.application.routes.draw do
|
|||
namespace :settings do
|
||||
resources :background_jobs, only: %i[index create destroy]
|
||||
resources :users, only: %i[index create destroy edit update]
|
||||
resources :maps, only: %i[index]
|
||||
patch 'maps', to: 'maps#update'
|
||||
end
|
||||
|
||||
patch 'settings', to: 'settings#update'
|
||||
|
|
@ -94,6 +96,10 @@ Rails.application.routes.draw do
|
|||
get 'thumbnail', constraints: { id: %r{[^/]+} }
|
||||
end
|
||||
end
|
||||
|
||||
namespace :maps do
|
||||
resources :tile_usage, only: [:create]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ fi
|
|||
|
||||
# Wait for the database to become available
|
||||
echo "⏳ Waiting for database to be ready..."
|
||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
|
||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
|
||||
>&2 echo "Postgres is unavailable - retrying..."
|
||||
sleep 2
|
||||
done
|
||||
|
|
|
|||
|
|
@ -29,14 +29,14 @@ rm -f $APP_PATH/tmp/pids/server.pid
|
|||
|
||||
# Wait for the database to become available
|
||||
echo "⏳ Waiting for database to be ready..."
|
||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c '\q'; do
|
||||
until PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c '\q'; do
|
||||
>&2 echo "Postgres is unavailable - retrying..."
|
||||
sleep 2
|
||||
done
|
||||
echo "✅ PostgreSQL is ready!"
|
||||
|
||||
# Create database if it doesn't exist
|
||||
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -d "$DATABASE_NAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
|
||||
if ! PGPASSWORD=$DATABASE_PASSWORD psql -h "$DATABASE_HOST" -p "$DATABASE_PORT" -U "$DATABASE_USERNAME" -c "SELECT 1 FROM pg_database WHERE datname='$DATABASE_NAME'" | grep -q 1; then
|
||||
echo "Creating database $DATABASE_NAME..."
|
||||
bundle exec rails db:create
|
||||
fi
|
||||
|
|
|
|||
36
spec/requests/api/v1/maps/tile_usage_spec.rb
Normal file
36
spec/requests/api/v1/maps/tile_usage_spec.rb
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Api::V1::Maps::TileUsage', type: :request do
|
||||
describe 'POST /api/v1/maps/tile_usage' do
|
||||
let(:tile_count) { 5 }
|
||||
let(:track_service) { instance_double(Maps::TileUsage::Track) }
|
||||
let(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
allow(Maps::TileUsage::Track).to receive(:new).with(user.id, tile_count).and_return(track_service)
|
||||
allow(track_service).to receive(:call)
|
||||
end
|
||||
|
||||
context 'when user is authenticated' do
|
||||
it 'tracks tile usage' do
|
||||
post '/api/v1/maps/tile_usage',
|
||||
params: { tile_usage: { count: tile_count } },
|
||||
headers: { 'Authorization' => "Bearer #{user.api_key}" }
|
||||
|
||||
expect(Maps::TileUsage::Track).to have_received(:new).with(user.id, tile_count)
|
||||
expect(track_service).to have_received(:call)
|
||||
expect(response).to have_http_status(:ok)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not authenticated' do
|
||||
it 'returns unauthorized' do
|
||||
post '/api/v1/maps/tile_usage', params: { tile_usage: { count: tile_count } }
|
||||
|
||||
expect(response).to have_http_status(:unauthorized)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
43
spec/requests/settings/maps_spec.rb
Normal file
43
spec/requests/settings/maps_spec.rb
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'settings/maps', type: :request do
|
||||
before do
|
||||
stub_request(:any, 'https://api.github.com/repos/Freika/dawarich/tags')
|
||||
.to_return(status: 200, body: '[{"name": "1.0.0"}]', headers: {})
|
||||
end
|
||||
|
||||
context 'when user is authenticated' do
|
||||
let!(:user) { create(:user) }
|
||||
|
||||
before do
|
||||
sign_in user
|
||||
end
|
||||
|
||||
describe 'GET /index' do
|
||||
it 'returns a success response' do
|
||||
get settings_maps_url
|
||||
|
||||
expect(response).to be_successful
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PATCH /update' do
|
||||
it 'returns a success response' do
|
||||
patch settings_maps_path, params: { maps: { name: 'Test', url: 'https://test.com' } }
|
||||
|
||||
expect(response).to redirect_to(settings_maps_path)
|
||||
expect(user.settings['maps']).to eq({ 'name' => 'Test', 'url' => 'https://test.com' })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not authenticated' do
|
||||
it 'redirects to the sign in page' do
|
||||
get settings_maps_path
|
||||
|
||||
expect(response).to redirect_to(new_user_session_path)
|
||||
end
|
||||
end
|
||||
end
|
||||
42
spec/services/maps/tile_usage/track_spec.rb
Normal file
42
spec/services/maps/tile_usage/track_spec.rb
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'prometheus_exporter/client'
|
||||
|
||||
RSpec.describe Maps::TileUsage::Track do
|
||||
describe '#call' do
|
||||
subject(:track) { described_class.new(user_id, tile_count).call }
|
||||
|
||||
let(:user_id) { 1 }
|
||||
let(:tile_count) { 5 }
|
||||
let(:prometheus_client) { instance_double(PrometheusExporter::Client) }
|
||||
|
||||
before do
|
||||
allow(PrometheusExporter::Client).to receive(:default).and_return(prometheus_client)
|
||||
allow(prometheus_client).to receive(:send_json)
|
||||
allow(DawarichSettings).to receive(:prometheus_exporter_enabled?).and_return(true)
|
||||
end
|
||||
|
||||
it 'tracks tile usage in prometheus' do
|
||||
expect(prometheus_client).to receive(:send_json).with(
|
||||
{
|
||||
type: 'counter',
|
||||
name: 'dawarich_map_tiles_usage',
|
||||
value: tile_count
|
||||
}
|
||||
)
|
||||
|
||||
track
|
||||
end
|
||||
|
||||
it 'tracks tile usage in cache' do
|
||||
expect(Rails.cache).to receive(:write).with(
|
||||
"dawarich_map_tiles_usage:#{user_id}:#{Time.zone.today}",
|
||||
tile_count,
|
||||
expires_in: 7.days
|
||||
)
|
||||
|
||||
track
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -70,7 +70,7 @@ RSpec.describe Photos::Thumbnail do
|
|||
let(:source) { 'unsupported' }
|
||||
|
||||
it 'raises an error' do
|
||||
expect { subject }.to raise_error(RuntimeError, 'Unsupported source: unsupported')
|
||||
expect { subject }.to raise_error(ArgumentError, 'Unsupported source: unsupported')
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
143
spec/services/users/safe_settings_spec.rb
Normal file
143
spec/services/users/safe_settings_spec.rb
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe Users::SafeSettings do
|
||||
describe '#config' do
|
||||
context 'with default values' do
|
||||
let(:settings) { {} }
|
||||
let(:safe_settings) { described_class.new(settings) }
|
||||
|
||||
it 'returns default configuration' do
|
||||
expect(safe_settings.config).to eq(
|
||||
{
|
||||
fog_of_war_meters: 50,
|
||||
meters_between_routes: 500,
|
||||
preferred_map_layer: 'OpenStreetMap',
|
||||
speed_colored_routes: false,
|
||||
points_rendering_mode: 'raw',
|
||||
minutes_between_routes: 30,
|
||||
time_threshold_minutes: 30,
|
||||
merge_threshold_minutes: 15,
|
||||
live_map_enabled: true,
|
||||
route_opacity: 0.6,
|
||||
immich_url: nil,
|
||||
immich_api_key: nil,
|
||||
photoprism_url: nil,
|
||||
photoprism_api_key: nil,
|
||||
maps: {}
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with custom values' do
|
||||
let(:settings) do
|
||||
{
|
||||
'fog_of_war_meters' => 100,
|
||||
'meters_between_routes' => 1000,
|
||||
'preferred_map_layer' => 'Satellite',
|
||||
'speed_colored_routes' => true,
|
||||
'points_rendering_mode' => 'simplified',
|
||||
'minutes_between_routes' => 60,
|
||||
'time_threshold_minutes' => 45,
|
||||
'merge_threshold_minutes' => 20,
|
||||
'live_map_enabled' => false,
|
||||
'route_opacity' => 0.8,
|
||||
'immich_url' => 'https://immich.example.com',
|
||||
'immich_api_key' => 'immich-key',
|
||||
'photoprism_url' => 'https://photoprism.example.com',
|
||||
'photoprism_api_key' => 'photoprism-key',
|
||||
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' }
|
||||
}
|
||||
end
|
||||
let(:safe_settings) { described_class.new(settings) }
|
||||
|
||||
it 'returns custom configuration' do
|
||||
expect(safe_settings.config).to eq(
|
||||
{
|
||||
fog_of_war_meters: 100,
|
||||
meters_between_routes: 1000,
|
||||
preferred_map_layer: 'Satellite',
|
||||
speed_colored_routes: true,
|
||||
points_rendering_mode: 'simplified',
|
||||
minutes_between_routes: 60,
|
||||
time_threshold_minutes: 45,
|
||||
merge_threshold_minutes: 20,
|
||||
live_map_enabled: false,
|
||||
route_opacity: 0.8,
|
||||
immich_url: 'https://immich.example.com',
|
||||
immich_api_key: 'immich-key',
|
||||
photoprism_url: 'https://photoprism.example.com',
|
||||
photoprism_api_key: 'photoprism-key',
|
||||
maps: { 'name' => 'custom', 'url' => 'https://custom.example.com' }
|
||||
}
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'individual settings' do
|
||||
let(:safe_settings) { described_class.new(settings) }
|
||||
|
||||
context 'with default values' do
|
||||
let(:settings) { {} }
|
||||
|
||||
it 'returns default values for each setting' do
|
||||
expect(safe_settings.fog_of_war_meters).to eq(50)
|
||||
expect(safe_settings.meters_between_routes).to eq(500)
|
||||
expect(safe_settings.preferred_map_layer).to eq('OpenStreetMap')
|
||||
expect(safe_settings.speed_colored_routes).to be false
|
||||
expect(safe_settings.points_rendering_mode).to eq('raw')
|
||||
expect(safe_settings.minutes_between_routes).to eq(30)
|
||||
expect(safe_settings.time_threshold_minutes).to eq(30)
|
||||
expect(safe_settings.merge_threshold_minutes).to eq(15)
|
||||
expect(safe_settings.live_map_enabled).to be true
|
||||
expect(safe_settings.route_opacity).to eq(0.6)
|
||||
expect(safe_settings.immich_url).to be_nil
|
||||
expect(safe_settings.immich_api_key).to be_nil
|
||||
expect(safe_settings.photoprism_url).to be_nil
|
||||
expect(safe_settings.photoprism_api_key).to be_nil
|
||||
expect(safe_settings.maps).to eq({})
|
||||
end
|
||||
end
|
||||
|
||||
context 'with custom values' do
|
||||
let(:settings) do
|
||||
{
|
||||
'fog_of_war_meters' => 100,
|
||||
'meters_between_routes' => 1000,
|
||||
'preferred_map_layer' => 'Satellite',
|
||||
'speed_colored_routes' => true,
|
||||
'points_rendering_mode' => 'simplified',
|
||||
'minutes_between_routes' => 60,
|
||||
'time_threshold_minutes' => 45,
|
||||
'merge_threshold_minutes' => 20,
|
||||
'live_map_enabled' => false,
|
||||
'route_opacity' => 0.8,
|
||||
'immich_url' => 'https://immich.example.com',
|
||||
'immich_api_key' => 'immich-key',
|
||||
'photoprism_url' => 'https://photoprism.example.com',
|
||||
'photoprism_api_key' => 'photoprism-key',
|
||||
'maps' => { 'name' => 'custom', 'url' => 'https://custom.example.com' }
|
||||
}
|
||||
end
|
||||
|
||||
it 'returns custom values for each setting' do
|
||||
expect(safe_settings.fog_of_war_meters).to eq(100)
|
||||
expect(safe_settings.meters_between_routes).to eq(1000)
|
||||
expect(safe_settings.preferred_map_layer).to eq('Satellite')
|
||||
expect(safe_settings.speed_colored_routes).to be true
|
||||
expect(safe_settings.points_rendering_mode).to eq('simplified')
|
||||
expect(safe_settings.minutes_between_routes).to eq(60)
|
||||
expect(safe_settings.time_threshold_minutes).to eq(45)
|
||||
expect(safe_settings.merge_threshold_minutes).to eq(20)
|
||||
expect(safe_settings.live_map_enabled).to be false
|
||||
expect(safe_settings.route_opacity).to eq(0.8)
|
||||
expect(safe_settings.immich_url).to eq('https://immich.example.com')
|
||||
expect(safe_settings.immich_api_key).to eq('immich-key')
|
||||
expect(safe_settings.photoprism_url).to eq('https://photoprism.example.com')
|
||||
expect(safe_settings.photoprism_api_key).to eq('photoprism-key')
|
||||
expect(safe_settings.maps).to eq({ 'name' => 'custom', 'url' => 'https://custom.example.com' })
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Reference in a new issue