diff --git a/CHANGELOG.md b/CHANGELOG.md index b0aff878..45b4e351 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ 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.37.2] - 2026-01-03 +# [0.37.2] - 2026-01-04 ## Fixed @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Time spent in a country and city is now calculated correctly for the year-end digest email. #2104 - Updated Trix to fix a XSS vulnerability. #2102 - Map v2 UI no longer blocks when Immich/Photoprism integration has a bad URL or is unreachable. Added 10-second timeout to photo API requests and improved error handling to prevent UI freezing during initial load. #2085 +- In Map v2 settings, you can now enable map to be rendered as a globe. # [0.37.1] - 2025-12-30 diff --git a/app/controllers/api/v1/settings_controller.rb b/app/controllers/api/v1/settings_controller.rb index f164bbe1..9d2f7805 100644 --- a/app/controllers/api/v1/settings_controller.rb +++ b/app/controllers/api/v1/settings_controller.rb @@ -31,7 +31,7 @@ class Api::V1::SettingsController < ApiController :preferred_map_layer, :points_rendering_mode, :live_map_enabled, :immich_url, :immich_api_key, :photoprism_url, :photoprism_api_key, :speed_colored_routes, :speed_color_scale, :fog_of_war_threshold, - :maps_v2_style, :maps_maplibre_style, + :maps_v2_style, :maps_maplibre_style, :globe_projection, enabled_map_layers: [] ) end diff --git a/app/javascript/controllers/maps/maplibre/map_initializer.js b/app/javascript/controllers/maps/maplibre/map_initializer.js index b253135e..6952c1ea 100644 --- a/app/javascript/controllers/maps/maplibre/map_initializer.js +++ b/app/javascript/controllers/maps/maplibre/map_initializer.js @@ -16,17 +16,35 @@ export class MapInitializer { mapStyle = 'streets', center = [0, 0], zoom = 2, - showControls = true + showControls = true, + globeProjection = false } = settings const style = await getMapStyle(mapStyle) - const map = new maplibregl.Map({ + const mapOptions = { container, style, center, zoom - }) + } + + const map = new maplibregl.Map(mapOptions) + + // Set globe projection after map loads + if (globeProjection === true || globeProjection === 'true') { + map.on('load', () => { + map.setProjection({ type: 'globe' }) + + // Add atmosphere effect + map.setSky({ + 'atmosphere-blend': [ + 'interpolate', ['linear'], ['zoom'], + 0, 1, 5, 1, 7, 0 + ] + }) + }) + } if (showControls) { map.addControl(new maplibregl.NavigationControl(), 'top-right') diff --git a/app/javascript/controllers/maps/maplibre/settings_manager.js b/app/javascript/controllers/maps/maplibre/settings_manager.js index 02c7ae88..d2e77ebf 100644 --- a/app/javascript/controllers/maps/maplibre/settings_manager.js +++ b/app/javascript/controllers/maps/maplibre/settings_manager.js @@ -91,6 +91,11 @@ export class SettingsController { mapStyleSelect.value = this.settings.mapStyle || 'light' } + // Sync globe projection toggle + if (controller.hasGlobeToggleTarget) { + controller.globeToggleTarget.checked = this.settings.globeProjection || false + } + // Sync fog of war settings const fogRadiusInput = controller.element.querySelector('input[name="fogOfWarRadius"]') if (fogRadiusInput) { @@ -178,6 +183,22 @@ export class SettingsController { } } + /** + * Toggle globe projection + * Requires page reload to apply since projection is set at map initialization + */ + async toggleGlobe(event) { + const enabled = event.target.checked + await SettingsManager.updateSetting('globeProjection', enabled) + + Toast.info('Globe view will be applied after page reload') + + // Prompt user to reload + if (confirm('Globe view requires a page reload to take effect. Reload now?')) { + window.location.reload() + } + } + /** * Update route opacity in real-time */ diff --git a/app/javascript/controllers/maps/maplibre_controller.js b/app/javascript/controllers/maps/maplibre_controller.js index 5f553d70..57fbe5b4 100644 --- a/app/javascript/controllers/maps/maplibre_controller.js +++ b/app/javascript/controllers/maps/maplibre_controller.js @@ -64,6 +64,8 @@ export default class extends Controller { 'speedColoredToggle', 'speedColorScaleContainer', 'speedColorScaleInput', + // Globe projection + 'globeToggle', // Family members 'familyMembersList', 'familyMembersContainer', @@ -147,7 +149,8 @@ export default class extends Controller { */ async initializeMap() { this.map = await MapInitializer.initialize(this.containerTarget, { - mapStyle: this.settings.mapStyle + mapStyle: this.settings.mapStyle, + globeProjection: this.settings.globeProjection }) } @@ -243,6 +246,7 @@ export default class extends Controller { updateFogThresholdDisplay(event) { return this.settingsController.updateFogThresholdDisplay(event) } updateMetersBetweenDisplay(event) { return this.settingsController.updateMetersBetweenDisplay(event) } updateMinutesBetweenDisplay(event) { return this.settingsController.updateMinutesBetweenDisplay(event) } + toggleGlobe(event) { return this.settingsController.toggleGlobe(event) } // Area Selection Manager methods startSelectArea() { return this.areaSelectionManager.startSelectArea() } diff --git a/app/javascript/maps_maplibre/utils/settings_manager.js b/app/javascript/maps_maplibre/utils/settings_manager.js index a5058e27..aa12d3e8 100644 --- a/app/javascript/maps_maplibre/utils/settings_manager.js +++ b/app/javascript/maps_maplibre/utils/settings_manager.js @@ -14,7 +14,8 @@ const DEFAULT_SETTINGS = { minutesBetweenRoutes: 60, pointsRenderingMode: 'raw', speedColoredRoutes: false, - speedColorScale: '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300' + speedColorScale: '0:#00ff00|15:#00ffff|30:#ff00ff|50:#ffff00|100:#ff3300', + globeProjection: false } // Mapping between v2 layer names and v1 layer names in enabled_map_layers array @@ -41,7 +42,8 @@ const BACKEND_SETTINGS_MAP = { minutesBetweenRoutes: 'minutes_between_routes', pointsRenderingMode: 'points_rendering_mode', speedColoredRoutes: 'speed_colored_routes', - speedColorScale: 'speed_color_scale' + speedColorScale: 'speed_color_scale', + globeProjection: 'globe_projection' } export class SettingsManager { @@ -152,6 +154,8 @@ export class SettingsManager { value = parseInt(value) || DEFAULT_SETTINGS.minutesBetweenRoutes } else if (frontendKey === 'speedColoredRoutes') { value = value === true || value === 'true' + } else if (frontendKey === 'globeProjection') { + value = value === true || value === 'true' } frontendSettings[frontendKey] = value @@ -219,6 +223,8 @@ export class SettingsManager { value = parseInt(value).toString() } else if (frontendKey === 'speedColoredRoutes') { value = Boolean(value) + } else if (frontendKey === 'globeProjection') { + value = Boolean(value) } backendSettings[backendKey] = value diff --git a/app/services/users/safe_settings.rb b/app/services/users/safe_settings.rb index a2e91f7b..55d5c62d 100644 --- a/app/services/users/safe_settings.rb +++ b/app/services/users/safe_settings.rb @@ -22,7 +22,8 @@ class Users::SafeSettings 'visits_suggestions_enabled' => 'true', 'enabled_map_layers' => %w[Routes Heatmap], 'maps_maplibre_style' => 'light', - 'digest_emails_enabled' => true + 'digest_emails_enabled' => true, + 'globe_projection' => false }.freeze def initialize(settings = {}) @@ -52,7 +53,8 @@ class Users::SafeSettings speed_color_scale: speed_color_scale, fog_of_war_threshold: fog_of_war_threshold, enabled_map_layers: enabled_map_layers, - maps_maplibre_style: maps_maplibre_style + maps_maplibre_style: maps_maplibre_style, + globe_projection: globe_projection } end # rubocop:enable Metrics/MethodLength @@ -141,6 +143,10 @@ class Users::SafeSettings settings['maps_maplibre_style'] end + def globe_projection + ActiveModel::Type::Boolean.new.cast(settings['globe_projection']) + end + def digest_emails_enabled? value = settings['digest_emails_enabled'] return true if value.nil? diff --git a/app/views/map/maplibre/_settings_panel.html.erb b/app/views/map/maplibre/_settings_panel.html.erb index 356c90a6..143718f5 100644 --- a/app/views/map/maplibre/_settings_panel.html.erb +++ b/app/views/map/maplibre/_settings_panel.html.erb @@ -365,6 +365,19 @@ + +
+ +

Render map as a 3D globe (requires page reload)

+
+