Merge branch 'Freika:master' into fix/import-google-timeline

This commit is contained in:
Sascha Zepter 2024-09-30 22:38:16 +02:00 committed by GitHub
commit 2231c0aab9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 461 additions and 304 deletions

View file

@ -1 +1 @@
0.14.4
0.14.5

View file

@ -5,6 +5,20 @@ 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.14.5] - 2024-09-28
### Fixed
- GPX export now finishes correctly and does not throw an error in the end
- Deleting points from the Points page now preserves `start_at` and `end_at` values for the routes. #261
- Visits map now being rendered correctly in the Visits page. #262
- Fixed issue with timezones for negative UTC offsets. #194, #122
- Point page is no longer reloads losing provided timestamps when searching for points on Points page. #283
### Changed
- Map layers from Stadia were disabled for now due to necessary API key
# [0.14.4] - 2024-09-24
### Fixed

View file

@ -14,7 +14,7 @@
#### **Did you fix whitespace, format code, or make a purely cosmetic patch?**
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Dawarich will generally not be accepted.
Changes that are cosmetic in nature and do not add anything substantial to the stability, functionality, or testability of Dawarich will generally not be accepted. Same goes for chore changes, like updating dependencies, fixing typos, etc.
#### **Do you intend to add a new feature or change an existing one?**

File diff suppressed because one or more lines are too long

View file

@ -4,14 +4,12 @@ class PointsController < ApplicationController
before_action :authenticate_user!
def index
order_by = params[:order_by] || 'desc'
@points = points
.without_raw_data
.where(timestamp: start_at..end_at)
.order(timestamp: order_by)
.page(params[:page])
.per(50)
.without_raw_data
.where(timestamp: start_at..end_at)
.order(timestamp: order_by)
.page(params[:page])
.per(50)
@start_at = Time.zone.at(start_at)
@end_at = Time.zone.at(end_at)
@ -22,7 +20,9 @@ class PointsController < ApplicationController
def bulk_destroy
current_user.tracked_points.where(id: params[:point_ids].compact).destroy_all
redirect_to points_url, notice: 'Points were successfully destroyed.', status: :see_other
redirect_to points_url(preserved_params),
notice: 'Points were successfully destroyed.',
status: :see_other
end
private
@ -44,14 +44,22 @@ class PointsController < ApplicationController
end
def points
params[:import_id] ? points_from_import : points_from_user
params[:import_id].present? ? import_points : user_points
end
def points_from_import
def import_points
current_user.imports.find(params[:import_id]).points
end
def points_from_user
def user_points
current_user.tracked_points
end
def order_by
params[:order_by] || 'desc'
end
def preserved_params
params.to_enum.to_h.with_indifferent_access.slice(:start_at, :end_at, :order_by, :import_id)
end
end

View file

@ -11,14 +11,14 @@ module ApplicationHelper
end
def year_timespan(year)
start_at = Time.utc(year).in_time_zone(Time.zone).beginning_of_year.strftime('%Y-%m-%dT%H:%M')
end_at = Time.utc(year).in_time_zone(Time.zone).end_of_year.strftime('%Y-%m-%dT%H:%M')
start_at = Time.new(year).beginning_of_year.strftime('%Y-%m-%dT%H:%M')
end_at = Time.new(year).end_of_year.strftime('%Y-%m-%dT%H:%M')
{ start_at:, end_at: }
end
def timespan(month, year)
month = DateTime.new(year, month).in_time_zone(Time.zone)
month = DateTime.new(year, month)
start_at = month.beginning_of_month.to_time.strftime('%Y-%m-%dT%H:%M')
end_at = month.end_of_month.to_time.strftime('%Y-%m-%dT%H:%M')

View file

@ -10,16 +10,16 @@ import { osmMapLayer } from "../maps/layers";
import { osmHotMapLayer } from "../maps/layers";
import { OPNVMapLayer } from "../maps/layers";
import { openTopoMapLayer } from "../maps/layers";
import { stadiaAlidadeSmoothMapLayer } from "../maps/layers";
import { stadiaAlidadeSmoothDarkMapLayer } from "../maps/layers";
import { stadiaAlidadeSatelliteMapLayer } from "../maps/layers";
import { stadiaOsmBrightMapLayer } from "../maps/layers";
import { stadiaOutdoorMapLayer } from "../maps/layers";
import { stadiaStamenTonerMapLayer } from "../maps/layers";
import { stadiaStamenTonerBackgroundMapLayer } from "../maps/layers";
import { stadiaStamenTonerLiteMapLayer } from "../maps/layers";
import { stadiaStamenWatercolorMapLayer } from "../maps/layers";
import { stadiaStamenTerrainMapLayer } from "../maps/layers";
// import { stadiaAlidadeSmoothMapLayer } from "../maps/layers";
// import { stadiaAlidadeSmoothDarkMapLayer } from "../maps/layers";
// import { stadiaAlidadeSatelliteMapLayer } from "../maps/layers";
// import { stadiaOsmBrightMapLayer } from "../maps/layers";
// import { stadiaOutdoorMapLayer } from "../maps/layers";
// import { stadiaStamenTonerMapLayer } from "../maps/layers";
// import { stadiaStamenTonerBackgroundMapLayer } from "../maps/layers";
// import { stadiaStamenTonerLiteMapLayer } from "../maps/layers";
// import { stadiaStamenWatercolorMapLayer } from "../maps/layers";
// import { stadiaStamenTerrainMapLayer } from "../maps/layers";
import { cyclOsmMapLayer } from "../maps/layers";
import { esriWorldStreetMapLayer } from "../maps/layers";
import { esriWorldTopoMapLayer } from "../maps/layers";
@ -143,16 +143,16 @@ console.log(selectedLayerName);
"OpenStreetMap.HOT": osmHotMapLayer(this.map, selectedLayerName),
OPNV: OPNVMapLayer(this.map, selectedLayerName),
openTopo: openTopoMapLayer(this.map, selectedLayerName),
stadiaAlidadeSmooth: stadiaAlidadeSmoothMapLayer(this.map, selectedLayerName),
stadiaAlidadeSmoothDark: stadiaAlidadeSmoothDarkMapLayer(this.map, selectedLayerName),
stadiaAlidadeSatellite: stadiaAlidadeSatelliteMapLayer(this.map, selectedLayerName),
stadiaOsmBright: stadiaOsmBrightMapLayer(this.map, selectedLayerName),
stadiaOutdoor: stadiaOutdoorMapLayer(this.map, selectedLayerName),
stadiaStamenToner: stadiaStamenTonerMapLayer(this.map, selectedLayerName),
stadiaStamenTonerBackground: stadiaStamenTonerBackgroundMapLayer(this.map, selectedLayerName),
stadiaStamenTonerLite: stadiaStamenTonerLiteMapLayer(this.map, selectedLayerName),
stadiaStamenWatercolor: stadiaStamenWatercolorMapLayer(this.map, selectedLayerName),
stadiaStamenTerrain: stadiaStamenTerrainMapLayer(this.map, selectedLayerName),
// stadiaAlidadeSmooth: stadiaAlidadeSmoothMapLayer(this.map, selectedLayerName),
// stadiaAlidadeSmoothDark: stadiaAlidadeSmoothDarkMapLayer(this.map, selectedLayerName),
// stadiaAlidadeSatellite: stadiaAlidadeSatelliteMapLayer(this.map, selectedLayerName),
// stadiaOsmBright: stadiaOsmBrightMapLayer(this.map, selectedLayerName),
// stadiaOutdoor: stadiaOutdoorMapLayer(this.map, selectedLayerName),
// stadiaStamenToner: stadiaStamenTonerMapLayer(this.map, selectedLayerName),
// stadiaStamenTonerBackground: stadiaStamenTonerBackgroundMapLayer(this.map, selectedLayerName),
// stadiaStamenTonerLite: stadiaStamenTonerLiteMapLayer(this.map, selectedLayerName),
// stadiaStamenWatercolor: stadiaStamenWatercolorMapLayer(this.map, selectedLayerName),
// stadiaStamenTerrain: stadiaStamenTerrainMapLayer(this.map, selectedLayerName),
cyclOsm: cyclOsmMapLayer(this.map, selectedLayerName),
esriWorldStreet: esriWorldStreetMapLayer(this.map, selectedLayerName),
esriWorldTopo: esriWorldTopoMapLayer(this.map, selectedLayerName),

View file

@ -14,7 +14,7 @@ export default class extends Controller {
this.radius = this.element.dataset.radius;
this.map = L.map(this.containerTarget).setView([this.center[0], this.center[1]], 17);
osmMapLayer(this.map),
osmMapLayer(this.map, "OpenStreetMap")
this.addMarkers();
L.circle([this.center[0], this.center[1]], {

View file

@ -57,165 +57,165 @@ export function openTopoMapLayer(map, selectedLayerName) {
}
}
export function stadiaAlidadeSmoothMapLayer(map, selectedLayerName) {
let layerName = 'stadiaAlidadeSmooth';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaAlidadeSmoothMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaAlidadeSmooth';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaAlidadeSmoothDarkMapLayer(map, selectedLayerName) {
let layerName = 'stadiaAlidadeSmoothDark';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaAlidadeSmoothDarkMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaAlidadeSmoothDark';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_smooth_dark/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaAlidadeSatelliteMapLayer(map, selectedLayerName) {
let layerName = 'stadiaAlidadeSatellite';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | &copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'jpg'
});
// export function stadiaAlidadeSatelliteMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaAlidadeSatellite';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/alidade_satellite/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; CNES, Distribution Airbus DS, © Airbus DS, © PlanetObserver (Contains Copernicus Data) | &copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'jpg'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaOsmBrightMapLayer(map, selectedLayerName) {
let layerName = 'stadiaOsmBright';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaOsmBrightMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaOsmBright';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/osm_bright/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaOutdoorMapLayer(map, selectedLayerName) {
let layerName = 'stadiaOutdoor';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaOutdoorMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaOutdoor';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/outdoors/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaStamenTonerMapLayer(map, selectedLayerName) {
let layerName = 'stadiaStamenToner';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaStamenTonerMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaStamenToner';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaStamenTonerBackgroundMapLayer(map, selectedLayerName) {
let layerName = 'stadiaStamenTonerBackground';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_background/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaStamenTonerBackgroundMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaStamenTonerBackground';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_background/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaStamenTonerLiteMapLayer(map, selectedLayerName) {
let layerName = 'stadiaStamenTonerLite';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 20,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaStamenTonerLiteMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaStamenTonerLite';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_toner_lite/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 20,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaStamenWatercolorMapLayer(map, selectedLayerName) {
let layerName = 'stadiaStamenWatercolor';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.{ext}', {
minZoom: 1,
maxZoom: 16,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'jpg'
});
// export function stadiaStamenWatercolorMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaStamenWatercolor';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_watercolor/{z}/{x}/{y}.{ext}', {
// minZoom: 1,
// maxZoom: 16,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'jpg'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function stadiaStamenTerrainMapLayer(map, selectedLayerName) {
let layerName = 'stadiaStamenTerrain';
let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}{r}.{ext}', {
minZoom: 0,
maxZoom: 18,
attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
ext: 'png'
});
// export function stadiaStamenTerrainMapLayer(map, selectedLayerName) {
// let layerName = 'stadiaStamenTerrain';
// let layer = L.tileLayer('https://tiles.stadiamaps.com/tiles/stamen_terrain/{z}/{x}/{y}{r}.{ext}', {
// minZoom: 0,
// maxZoom: 18,
// attribution: '&copy; <a href="https://www.stadiamaps.com/" target="_blank">Stadia Maps</a> &copy; <a href="https://www.stamen.com/" target="_blank">Stamen Design</a> &copy; <a href="https://openmaptiles.org/" target="_blank">OpenMapTiles</a> &copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
// ext: 'png'
// });
if (selectedLayerName === layerName) {
return layer.addTo(map);
} else {
return layer;
}
}
// if (selectedLayerName === layerName) {
// return layer.addTo(map);
// } else {
// return layer;
// }
// }
export function cyclOsmMapLayer(map, selectedLayerName) {
let layerName = 'cyclOsm';

View file

@ -12,7 +12,7 @@ class Points::GpxSerializer
gpx.waypoints << GPX::Waypoint.new(
lat: point.latitude.to_f,
lon: point.longitude.to_f,
time: point.recorded_at.strftime('%FT%R:%SZ'),
time: point.recorded_at,
ele: point.altitude.to_f
)
end

View file

@ -58,7 +58,7 @@ class Exports::Create
def points_data(points)
case file_format.to_sym
when :json then process_geojson_export(points)
when :gpx then process_gpx_export(points)
when :gpx then process_gpx_export(points)
else raise ArgumentError, "Unsupported file format: #{file_format}"
end
end
@ -75,6 +75,7 @@ class Exports::Create
dir_path = Rails.root.join('public', 'exports')
Dir.mkdir(dir_path) unless Dir.exist?(dir_path)
file_path = dir_path.join("#{export.name}.#{file_format}")
File.open(file_path, 'w') { |file| file.write(data) }
end
end

View file

@ -57,15 +57,15 @@ class GoogleMaps::SemanticHistoryParser
}
end
elsif timeline_object['placeVisit'].present?
if timeline_object['placeVisit']['location']['latitudeE7'].present? &&
timeline_object['placeVisit']['location']['longitudeE7'].present?
if timeline_object.dig('placeVisit', 'location', 'latitudeE7').present? &&
timeline_object.dig('placeVisit', 'location', 'longitudeE7').present?
{
latitude: timeline_object['placeVisit']['location']['latitudeE7'].to_f / 10**7,
longitude: timeline_object['placeVisit']['location']['longitudeE7'].to_f / 10**7,
timestamp: Timestamps::parse_timestamp(timeline_object['placeVisit']['duration']['startTimestamp'] || timeline_object['placeVisit']['duration']['startTimestampMs']),
raw_data: timeline_object
}
elsif timeline_object['placeVisit']['otherCandidateLocations'].any?
elsif timeline_object.dig('placeVisit', 'otherCandidateLocations')&.any?
point = timeline_object['placeVisit']['otherCandidateLocations'][0]
next unless point['latitudeE7'].present? && point['longitudeE7'].present?

View file

@ -20,7 +20,7 @@ class Visits::Suggest
create_visits_notification(user)
nil unless reverse_geocoding_enabled?
return nil unless reverse_geocoding_enabled?
reverse_geocode(visits)
end

View file

@ -1,7 +1,7 @@
<% content_for :title, 'Points' %>
<div class="w-full">
<%= form_with url: points_path(import_id: params[:import_id]), method: :get do |f| %>
<%= form_with url: points_path(import_id: params[:import_id]), data: { turbo_method: :get }, method: :get do |f| %>
<div class="flex flex-col md:flex-row md:space-x-4 md:items-end">
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2">
@ -28,12 +28,12 @@
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2 text-center">
<%= link_to 'Export as GeoJSON', exports_path(start_at: @start_at, end_at: @end_at, file_format: :json), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points withing timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
<%= link_to 'Export as GeoJSON', exports_path(start_at: @start_at, end_at: @end_at, file_format: :json), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
</div>
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2 text-center">
<%= link_to 'Export as GPX', exports_path(start_at: @start_at, end_at: @end_at, file_format: :gpx), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points withing timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
<%= link_to 'Export as GPX', exports_path(start_at: @start_at, end_at: @end_at, file_format: :gpx), data: { confirm: "Are you sure?", turbo_confirm: "Are you sure? This will start background process of exporting points within timeframe, selected between #{@start_at} and #{@end_at}", turbo_method: :post }, class: "px-4 py-2 bg-green-500 text-white rounded-md join-item" %>
</div>
</div>
</div>

View file

@ -9,7 +9,7 @@
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places', places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Places<sup>β</sup>', places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
</ul>
@ -44,7 +44,7 @@
<li><%= link_to 'Points', points_url, class: "#{active_class?(points_url)}" %></li>
<li><%= link_to 'Stats', stats_url, class: "#{active_class?(stats_url)}" %></li>
<li><%= link_to 'Visits<sup>β</sup>'.html_safe, visits_url(status: :confirmed), class: "#{active_class?(visits_url)}" %></li>
<li><%= link_to 'Places', places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Places<sup>β</sup>', places_url, class: "#{active_class?(places_url)}" %></li>
<li><%= link_to 'Imports', imports_url, class: "#{active_class?(imports_url)}" %></li>
<li><%= link_to 'Exports', exports_url, class: "#{active_class?(exports_url)}" %></li>
</ul>

View file

@ -8,11 +8,26 @@ Make sure to exclude "http://" or "https://" from the environment variable. ⚠
At the time of writing this, the way to set the environment variable is to edit the docker-compose.yml file. Find all APPLICATION_HOSTS entries in the docker-compose.yml file and make sure to include your domain name. Example:
```yaml
dawarich_app:
dawarich_app:
image: freikin/dawarich:latest
container_name: dawarich_app
...
environment:
APPLICATION_HOSTS: "yourhost.com,www.yourhost.com,127.0.0.1"
...
APPLICATION_HOST: "yourhost.com" <------------------------------ Edit this
APPLICATION_HOSTS: "yourhost.com,www.yourhost.com,127.0.0.1" <-- Edit this
```
```yaml
dawarich_sidekiq:
image: freikin/dawarich:latest
container_name: dawarich_sidekiq
...
environment:
...
APPLICATION_HOST: "yourhost.com" <------------------------------ Edit this
APPLICATION_HOSTS: "yourhost.com,www.yourhost.com,127.0.0.1" <-- Edit this
...
```
For a Synology install, refer to **[Synology Install Tutorial](How_to_install_Dawarich_on_Synology.md)**. In this page, it is explained how to set the APPLICATION_HOSTS environment variable.
@ -24,13 +39,42 @@ Now that the app works with a domain name, the server needs to be set up to use
Below are examples of reverse proxy configurations.
### Nginx
```
```nginx
server {
listen 80;
listen [::]:80;
server_name example.com;
brotli on;
brotli_comp_level 6;
brotli_types
text/css
text/plain
text/xml
text/x-component
text/javascript
application/x-javascript
application/javascript
application/json
application/manifest+json
application/vnd.api+json
application/xml
application/xhtml+xml
application/rss+xml
application/atom+xml
application/vnd.ms-fontobject
application/x-font-ttf
application/x-font-opentype
application/x-font-truetype
image/svg+xml
image/x-icon
image/vnd.microsoft.icon
font/ttf
font/eot
font/otf
font/opentype;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
@ -54,29 +98,34 @@ For Apache2, you might need to enable some modules. Start by entering the follow
sudo a2enmod proxy
sudo a2enmod proxy_http
sudo a2enmod headers
sudo a2enmod brotli
```
With the above commands entered, the configuration below should work properly.
```
```apache
<VirtualHost *:80>
ServerName example.com
ProxyRequests Off
ProxyPreserveHost On
<Proxy *>
Require all granted
</Proxy>
Header always set X-Real-IP %{REMOTE_ADDR}s
Header always set X-Forwarded-For %{REMOTE_ADDR}s
Header always set X-Forwarded-Proto https
Header always set X-Forwarded-Server %{SERVER_NAME}s
Header always set Host %{HTTP_HOST}s
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
ServerName example.com
ProxyRequests Off
ProxyPreserveHost On
<Proxy *>
Require all granted
</Proxy>
Header always set X-Real-IP %{REMOTE_ADDR}s
Header always set X-Forwarded-For %{REMOTE_ADDR}s
Header always set X-Forwarded-Proto https
Header always set X-Forwarded-Server %{SERVER_NAME}s
Header always set Host %{HTTP_HOST}s
SetOutputFilter BROTLI
AddOutputFilterByType BROTLI_COMPRESS text/css text/plain text/xml text/javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
BrotliCompressionQuality 6
ProxyPass / http://127.0.0.1:3000/
ProxyPassReverse / http://127.0.0.1:3000/
</VirtualHost>
```
@ -94,97 +143,35 @@ Second, create a Docker network for Dawarich to use as the backend network:
docker network create dawarich
```
Adjust your Dawarich docker-compose.yaml so that the web app is exposed to your new network and the backend Dawarich network:
```
version: '3'
Adjust the following part of your Dawarich docker-compose.yaml, so that the web app is exposed to your new network and the backend Dawarich network:
```yaml
networks:
dawarich:
frontend:
external: true
services:
dawarich_redis:
image: redis:7.0-alpine
command: redis-server
networks:
- dawarich
volumes:
- ./dawarich/redis:/var/shared/redis
dawarich_db:
image: postgres:14.2-alpine
container_name: dawarich_db
volumes:
- ./dawarich/db:/var/lib/postgresql/data
- ./dawarich/shared:/var/shared
networks:
- dawarich
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
dawarich_app:
image: freikin/dawarich:latest
container_name: dawarich_app
volumes:
- ./dawarich/gems:/usr/local/bundle/gems
- ./dawarich/public:/var/app/public
networks:
- dawarich
- frontend
stdin_open: true
tty: true
entrypoint: dev-entrypoint.sh
command: ['bin/dev']
restart: on-failure
environment:
RAILS_ENV: development
REDIS_URL: redis://dawarich_redis:6379/0
DATABASE_HOST: dawarich_db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
DATABASE_NAME: dawarich_development
MIN_MINUTES_SPENT_IN_CITY: 60
APPLICATION_HOSTS: <YOUR FQDN HERE (ex. dawarich.example.com)>
TIME_ZONE: America/New_York
depends_on:
- dawarich_db
- dawarich_redis
dawarich_sidekiq:
image: freikin/dawarich:latest
container_name: dawarich_sidekiq
volumes:
- ./dawarich/gems:/usr/local/bundle/gems
- ./dawarich/public:/var/app/public
networks:
- dawarich
stdin_open: true
tty: true
entrypoint: dev-entrypoint.sh
command: ['sidekiq']
restart: on-failure
environment:
RAILS_ENV: development
REDIS_URL: redis://dawarich_redis:6379/0
DATABASE_HOST: dawarich_db
DATABASE_USERNAME: postgres
DATABASE_PASSWORD: password
DATABASE_NAME: dawarich_development
APPLICATION_HOSTS: <YOUR FQDN HERE (ex. dawarich.example.com)>
depends_on:
- dawarich_db
- dawarich_redis
- dawarich_app
...
```
Lastly, edit your Caddy config as needed:
```
```caddy
{
http_port 80
https_port 443
}
<YOUR FQDN HERE (ex. dawarich.example.com)> {
timeline.example.com {
reverse_proxy dawarich_app:3000
encode brotli {
match {
content_type text/css text/plain text/xml text/x-component text/javascript application/x-javascript application/javascript application/json application/manifest+json application/vnd.api+json application/xml application/xhtml+xml application/rss+xml application/atom+xml application/vnd.ms-fontobject application/x-font-ttf application/x-font-opentype application/x-font-truetype image/svg+xml image/x-icon image/vnd.microsoft.icon font/ttf font/eot font/otf font/opentype
}
}
}
```
timeline.example.com is an example, use your own (sub) domain.
---

View file

@ -27,6 +27,11 @@ FactoryBot.define do
country { nil }
user
trait :with_known_location do
latitude { 55.755826 }
longitude { 37.6173 }
end
trait :with_geodata do
geodata do
{

File diff suppressed because one or more lines are too long

View file

@ -56,5 +56,12 @@ RSpec.describe '/points', type: :request do
expect(response).to redirect_to(points_url)
end
it 'preserves the start_at and end_at parameters' do
delete bulk_destroy_points_url,
params: { point_ids: [point1.id, point2.id], start_at: '2021-01-01', end_at: '2021-01-02' }
expect(response).to redirect_to(points_url(start_at: '2021-01-01', end_at: '2021-01-02'))
end
end
end

View file

@ -23,7 +23,7 @@ RSpec.describe Points::GpxSerializer do
point = points[index]
expect(waypoint.lat).to eq(point.latitude)
expect(waypoint.lon).to eq(point.longitude)
expect(waypoint.time).to eq(point.recorded_at.strftime('%FT%R:%SZ'))
expect(waypoint.time).to eq(point.recorded_at)
end
end
end

View file

@ -13,14 +13,16 @@ RSpec.describe Exports::Create do
let(:export_name) { "#{start_at.to_date}_#{end_at.to_date}" }
let(:export) { create(:export, user:, name: export_name, status: :created) }
let(:export_content) { Points::GeojsonSerializer.new(points).call }
let!(:points) { create_list(:point, 10, user:, timestamp: start_at.to_datetime.to_i) }
let!(:points) do
create_list(:point, 10, :with_known_location, user:, timestamp: start_at.to_datetime.to_i)
end
it 'writes the data to a file' do
create_export
file_path = Rails.root.join('spec/fixtures/files/geojson/export_same_points.json')
expect(File.read(file_path)).to eq(export_content)
expect(File.read(file_path).strip).to eq(export_content)
end
it 'updates the export url' do

View file

@ -4,5 +4,34 @@ require 'rails_helper'
RSpec.describe GoogleMaps::RecordsParser do
describe '#call' do
subject(:parser) { described_class.new(import).call(json) }
let(:import) { create(:import) }
let(:json) do
{
'latitudeE7' => 123_456_789,
'longitudeE7' => 123_456_789,
'timestamp' => Time.zone.now.to_s,
'altitude' => 0,
'velocity' => 0
}
end
it 'creates a point' do
expect { parser }.to change(Point, :count).by(1)
end
context 'when point already exists' do
before do
create(
:point, user: import.user, import:, latitude: 12.3456789, longitude: 12.3456789,
timestamp: Time.zone.now.to_i
)
end
it 'does not create a point' do
expect { parser }.not_to change(Point, :count)
end
end
end
end

View file

@ -4,5 +4,109 @@ require 'rails_helper'
RSpec.describe GoogleMaps::SemanticHistoryParser do
describe '#call' do
subject(:parser) { described_class.new(import, user.id).call }
let(:user) { create(:user) }
context 'when activitySegment is present' do
context 'when startLocation is blank' do
let(:import) { create(:import, raw_data: { 'timelineObjects' => [activity_segment] }) }
let(:activity_segment) do
{
'activitySegment' => {
'waypointPath' => {
'waypoints' => [
{ 'latE7' => 123_456_789, 'lngE7' => 123_456_789 }
]
},
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'creates a point' do
expect { parser }.to change(Point, :count).by(1)
end
context 'when waypointPath is blank' do
let(:activity_segment) do
{
'activitySegment' => {
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'does not create a point' do
expect { parser }.not_to change(Point, :count)
end
end
end
context 'when startLocation is present' do
let(:import) { create(:import, raw_data: { 'timelineObjects' => [activity_segment] }) }
let(:activity_segment) do
{
'activitySegment' => {
'startLocation' => { 'latitudeE7' => 123_456_789, 'longitudeE7' => 123_456_789 },
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'creates a point' do
expect { parser }.to change(Point, :count).by(1)
end
end
end
context 'when placeVisit is present' do
context 'when location with coordinates is present' do
let(:import) { create(:import, raw_data: { 'timelineObjects' => [place_visit] }) }
let(:place_visit) do
{
'placeVisit' => {
'location' => { 'latitudeE7' => 123_456_789, 'longitudeE7' => 123_456_789 },
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'creates a point' do
expect { parser }.to change(Point, :count).by(1)
end
end
context 'when location with coordinates is blank' do
let(:import) { create(:import, raw_data: { 'timelineObjects' => [place_visit] }) }
let(:place_visit) do
{
'placeVisit' => {
'location' => {},
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'does not create a point' do
expect { parser }.not_to change(Point, :count)
end
context 'when otherCandidateLocations is present' do
let(:place_visit) do
{
'placeVisit' => {
'otherCandidateLocations' => [{ 'latitudeE7' => 123_456_789, 'longitudeE7' => 123_456_789 }],
'duration' => { 'startTimestamp' => Time.zone.now.to_s }
}
}
end
it 'creates a point' do
expect { parser }.to change(Point, :count).by(1)
end
end
end
end
end
end

View file

@ -10,21 +10,21 @@ RSpec.describe Visits::Suggest do
let!(:points) do
[
create(:point, user:, timestamp: start_at),
create(:point, user:, timestamp: start_at + 5.minutes),
create(:point, user:, timestamp: start_at + 10.minutes),
create(:point, user:, timestamp: start_at + 15.minutes),
create(:point, user:, timestamp: start_at + 20.minutes),
create(:point, user:, timestamp: start_at + 25.minutes),
create(:point, user:, timestamp: start_at + 30.minutes),
create(:point, user:, timestamp: start_at + 35.minutes),
create(:point, user:, timestamp: start_at + 40.minutes),
create(:point, user:, timestamp: start_at + 45.minutes),
create(:point, user:, timestamp: start_at + 50.minutes),
create(:point, user:, timestamp: start_at + 55.minutes),
create(:point, user:, timestamp: start_at + 95.minutes),
create(:point, user:, timestamp: start_at + 100.minutes),
create(:point, user:, timestamp: start_at + 105.minutes)
create(:point, :with_known_location, user:, timestamp: start_at),
create(:point, :with_known_location, user:, timestamp: start_at + 5.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 10.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 15.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 20.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 25.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 30.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 35.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 40.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 45.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 50.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 55.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 95.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 100.minutes),
create(:point, :with_known_location, user:, timestamp: start_at + 105.minutes)
]
end