Implement GPX export

This commit is contained in:
Eugene Burmakin 2024-09-02 21:35:08 +02:00
parent 80b2f8831d
commit 942c84fb07
10 changed files with 70 additions and 35 deletions

View file

@ -5,6 +5,22 @@ 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/).
## [UNRELEASED] — 2024-08-28
### Added
- GeoJSON format is now available for exporting data.
- GPX format is now available for exporting data.
### Changed
- Default exporting format is now GeoJSON instead of Owntracks-like JSON. This will allow you to use the exported data in other applications that support GeoJSON format.
### TODO
- [ ] Importing GeoJSON
## [0.12.2] — 2024-08-28
### Added

View file

@ -9,6 +9,7 @@ gem 'chartkick'
gem 'data_migrate'
gem 'devise'
gem 'geocoder'
gem 'gpx'
gem 'httparty'
gem 'importmap-rails'
gem 'kaminari'

View file

@ -137,6 +137,9 @@ GEM
csv (>= 3.0.0)
globalid (1.2.1)
activesupport (>= 6.1)
gpx (1.1.1)
nokogiri (~> 1.7)
rake
hashdiff (1.1.0)
httparty (0.22.0)
csv
@ -433,6 +436,7 @@ DEPENDENCIES
ffaker
foreman
geocoder
gpx
httparty
importmap-rails
kaminari

View file

@ -3,7 +3,7 @@
class ExportJob < ApplicationJob
queue_as :exports
def perform(export_id, start_at, end_at, format: :geojson)
def perform(export_id, start_at, end_at, format: :json)
export = Export.find(export_id)
Exports::Create.new(export:, start_at:, end_at:, format:).call

View file

@ -0,0 +1,17 @@
# frozen_string_literal: true
class Points::GpxSerializer
def initialize(points)
@points = points
end
def call
geojson_data = Points::GeojsonSerializer.new(points).call
GPX::GeoJSON.convert_to_gpx(geojson_data:)
end
private
attr_reader :points
end

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class Exports::Create
def initialize(export:, start_at:, end_at:, format: :geojson)
def initialize(export:, start_at:, end_at:, format: :json)
@export = export
@user = export.user
@start_at = start_at.to_datetime
@ -12,29 +12,15 @@ class Exports::Create
def call
export.update!(status: :processing)
Rails.logger.debug "====Exporting data for #{user.email} from #{start_at} to #{end_at}"
points = time_framed_points
data = points_data(points)
Rails.logger.debug "====Exporting #{points.size} points"
data =
case format
when :geojson then process_geojson_export(points)
when :gpx then process_gpx_export(points)
else raise ArgumentError, "Unsupported format: #{format}"
end
file_path = Rails.root.join('public', 'exports', "#{export.name}.#{format}")
File.open(file_path, 'w') { |file| file.write(data) }
create_export_file(data)
export.update!(status: :completed, url: "exports/#{export.name}.#{format}")
create_export_finished_notification
rescue StandardError => e
Rails.logger.error("====Export failed to create: #{e.message}")
create_failed_export_notification(e)
export.update!(status: :failed)
@ -68,10 +54,25 @@ class Exports::Create
).call
end
def points_data(points)
case format
when :json then process_geojson_export(points)
when :gpx then process_gpx_export(points)
else raise ArgumentError, "Unsupported format: #{format}"
end
end
def process_geojson_export(points)
Points::GeojsonSerializer.new(points).call
end
def process_gpx_export(points)
Points::GpxSerializer.new(points).call
end
def create_export_file(data)
file_path = Rails.root.join('public', 'exports', "#{export.name}.#{format}")
File.open(file_path, 'w') { |file| file.write(data) }
end
end

View file

@ -22,7 +22,7 @@
</div>
<div class="w-full md:w-2/12">
<div class="flex flex-col space-y-2 text-center">
<%= link_to 'Export GeoJSON', exports_path(start_at: @start_at, end_at: @end_at, file_format: :geojson), 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 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" %>
</div>
</div>
<%# <div class="w-full md:w-2/12"> %>

File diff suppressed because one or more lines are too long

View file

@ -8,7 +8,7 @@ RSpec.describe ExportJob, type: :job do
let(:end_at) { Time.zone.now }
it 'calls the Exports::Create service class' do
expect(Exports::Create).to receive(:new).with(export:, start_at:, end_at:).and_call_original
expect(Exports::Create).to receive(:new).with(export:, start_at:, end_at:, format: :json).and_call_original
described_class.perform_now(export.id, start_at, end_at)
end

View file

@ -4,20 +4,21 @@ require 'rails_helper'
RSpec.describe Exports::Create do
describe '#call' do
subject(:create_export) { described_class.new(export:, start_at:, end_at:).call }
subject(:create_export) { described_class.new(export:, start_at:, end_at:, format:).call }
let(:user) { create(:user) }
let(:start_at) { DateTime.new(2021, 1, 1).to_s }
let(:end_at) { DateTime.new(2021, 1, 2).to_s }
let(:export_name) { "#{start_at.to_date}_#{end_at.to_date}" }
let(:export) { create(:export, user:, name: export_name, status: :created) }
let(:export_content) { ExportSerializer.new(points, user.email).call }
let!(:points) { create_list(:point, 10, user:, timestamp: start_at.to_datetime.to_i) }
let(:format) { :json }
let(:user) { create(:user) }
let(:start_at) { DateTime.new(2021, 1, 1).to_s }
let(:end_at) { DateTime.new(2021, 1, 2).to_s }
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) }
it 'writes the data to a file' do
create_export
file_path = Rails.root.join('public', 'exports', "#{export_name}.json")
file_path = Rails.root.join('spec/fixtures/files/geojson/export.json')
expect(File.read(file_path)).to eq(export_content)
end
@ -49,12 +50,6 @@ RSpec.describe Exports::Create do
expect(export.reload.failed?).to be_truthy
end
it 'logs the error' do
expect(Rails.logger).to receive(:error).with('====Export failed to create: StandardError')
create_export
end
it 'creates a notification' do
expect { create_export }.to change { Notification.count }.by(1)
end