mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-11 01:31:39 -05:00
Fix kml kmz import issues
This commit is contained in:
parent
bb980f2210
commit
389198da73
7 changed files with 144 additions and 30 deletions
|
|
@ -12,10 +12,12 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
## Fixed
|
## Fixed
|
||||||
|
|
||||||
- Cities visited during a trip are now being calculated correctly. #547 #641
|
- Cities visited during a trip are now being calculated correctly. #547 #641 #1686 #1976
|
||||||
- Points on the map are now show time in user's timezone. #580 #1035
|
- Points on the map are now show time in user's timezone. #580 #1035 #1682
|
||||||
- Date range inputs now handle pre-epoch dates gracefully by clamping to valid PostgreSQL integer range. #685
|
- Date range inputs now handle pre-epoch dates gracefully by clamping to valid PostgreSQL integer range. #685
|
||||||
- Redis client now also being configured so that it could connect via unix socket. #1970
|
- Redis client now also being configured so that it could connect via unix socket. #1970
|
||||||
|
- Importing KML files now creates points with correct timestamps. #1988
|
||||||
|
- Importing KMZ files now works correctly.
|
||||||
|
|
||||||
|
|
||||||
# [0.36.2] - 2025-12-06
|
# [0.36.2] - 2025-12-06
|
||||||
|
|
|
||||||
File diff suppressed because one or more lines are too long
|
|
@ -34,7 +34,7 @@ export default class extends BaseController {
|
||||||
|
|
||||||
const statusCell = row.querySelector('[data-status-display]');
|
const statusCell = row.querySelector('[data-status-display]');
|
||||||
if (statusCell && data.import.status) {
|
if (statusCell && data.import.status) {
|
||||||
statusCell.textContent = data.import.status;
|
statusCell.innerHTML = this.renderStatusBadge(data.import.status);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -47,4 +47,32 @@ export default class extends BaseController {
|
||||||
this.channel.unsubscribe();
|
this.channel.unsubscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
renderStatusBadge(status) {
|
||||||
|
const statusLower = status.toLowerCase();
|
||||||
|
|
||||||
|
switch(statusLower) {
|
||||||
|
case 'completed':
|
||||||
|
return `<span class="badge badge-success badge-sm gap-1">
|
||||||
|
<span class="text-xs">✓</span>
|
||||||
|
<span>Completed</span>
|
||||||
|
</span>`;
|
||||||
|
case 'processing':
|
||||||
|
return `<span class="badge badge-warning badge-sm gap-1">
|
||||||
|
<span class="loading loading-spinner loading-xs"></span>
|
||||||
|
<span>Processing</span>
|
||||||
|
</span>`;
|
||||||
|
case 'failed':
|
||||||
|
return `<span class="badge badge-error badge-sm gap-1">
|
||||||
|
<span class="text-xs">✕</span>
|
||||||
|
<span>Failed</span>
|
||||||
|
</span>`;
|
||||||
|
default:
|
||||||
|
return `<span class="badge badge-sm">${this.capitalize(status)}</span>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
capitalize(str) {
|
||||||
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -127,6 +127,15 @@ class Imports::SourceDetector
|
||||||
else
|
else
|
||||||
file_content
|
file_content
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Check if it's a KMZ file (ZIP archive)
|
||||||
|
if filename&.downcase&.end_with?('.kmz')
|
||||||
|
# KMZ files are ZIP archives, check for ZIP signature
|
||||||
|
# ZIP files start with "PK" (0x50 0x4B)
|
||||||
|
return content_to_check[0..1] == 'PK'
|
||||||
|
end
|
||||||
|
|
||||||
|
# For KML files, check XML structure
|
||||||
(
|
(
|
||||||
content_to_check.strip.start_with?('<?xml') ||
|
content_to_check.strip.start_with?('<?xml') ||
|
||||||
content_to_check.strip.start_with?('<kml')
|
content_to_check.strip.start_with?('<kml')
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
# frozen_string_literal: true
|
# frozen_string_literal: true
|
||||||
|
|
||||||
require 'rexml/document'
|
require 'rexml/document'
|
||||||
|
require 'zip'
|
||||||
|
|
||||||
class Kml::Importer
|
class Kml::Importer
|
||||||
include Imports::Broadcaster
|
include Imports::Broadcaster
|
||||||
|
|
@ -15,7 +16,7 @@ class Kml::Importer
|
||||||
end
|
end
|
||||||
|
|
||||||
def call
|
def call
|
||||||
file_content = load_file_content
|
file_content = load_kml_content
|
||||||
doc = REXML::Document.new(file_content)
|
doc = REXML::Document.new(file_content)
|
||||||
|
|
||||||
points_data = []
|
points_data = []
|
||||||
|
|
@ -42,8 +43,54 @@ class Kml::Importer
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
def load_kml_content
|
||||||
|
# Read content in binary mode for ZIP detection
|
||||||
|
content = if file_path && File.exist?(file_path)
|
||||||
|
File.binread(file_path)
|
||||||
|
else
|
||||||
|
downloader_content = Imports::SecureFileDownloader.new(import.file).download_with_verification
|
||||||
|
# Convert StringIO to String if needed
|
||||||
|
downloader_content.is_a?(StringIO) ? downloader_content.read : downloader_content
|
||||||
|
end
|
||||||
|
|
||||||
|
# Ensure we have a binary string
|
||||||
|
content.force_encoding('BINARY') if content.respond_to?(:force_encoding)
|
||||||
|
|
||||||
|
# Check if this is a KMZ file (ZIP archive) by checking for ZIP signature
|
||||||
|
# ZIP files start with "PK" (0x50 0x4B)
|
||||||
|
if content[0..1] == 'PK'
|
||||||
|
extract_kml_from_kmz(content)
|
||||||
|
else
|
||||||
|
content
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def extract_kml_from_kmz(kmz_content)
|
||||||
|
# KMZ files are ZIP archives containing a KML file (usually doc.kml)
|
||||||
|
# We need to extract the KML content from the ZIP
|
||||||
|
kml_content = nil
|
||||||
|
|
||||||
|
Zip::InputStream.open(StringIO.new(kmz_content)) do |io|
|
||||||
|
while (entry = io.get_next_entry)
|
||||||
|
if entry.name.downcase.end_with?('.kml')
|
||||||
|
kml_content = io.read
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
raise 'No KML file found in KMZ archive' unless kml_content
|
||||||
|
|
||||||
|
kml_content
|
||||||
|
rescue Zip::Error => e
|
||||||
|
raise "Failed to extract KML from KMZ: #{e.message}"
|
||||||
|
end
|
||||||
|
|
||||||
def parse_placemark(placemark)
|
def parse_placemark(placemark)
|
||||||
points = []
|
points = []
|
||||||
|
|
||||||
|
return points unless has_explicit_timestamp?(placemark)
|
||||||
|
|
||||||
timestamp = extract_timestamp(placemark)
|
timestamp = extract_timestamp(placemark)
|
||||||
|
|
||||||
# Handle Point geometry
|
# Handle Point geometry
|
||||||
|
|
@ -74,7 +121,6 @@ class Kml::Importer
|
||||||
end
|
end
|
||||||
|
|
||||||
def parse_gx_track(track)
|
def parse_gx_track(track)
|
||||||
# Google Earth Track extension with coordinated when/coord pairs
|
|
||||||
points = []
|
points = []
|
||||||
|
|
||||||
timestamps = []
|
timestamps = []
|
||||||
|
|
@ -89,28 +135,26 @@ class Kml::Importer
|
||||||
|
|
||||||
# Match timestamps with coordinates
|
# Match timestamps with coordinates
|
||||||
[timestamps.size, coordinates.size].min.times do |i|
|
[timestamps.size, coordinates.size].min.times do |i|
|
||||||
begin
|
time = Time.parse(timestamps[i]).to_i
|
||||||
time = Time.parse(timestamps[i]).to_i
|
coord_parts = coordinates[i].split(/\s+/)
|
||||||
coord_parts = coordinates[i].split(/\s+/)
|
next if coord_parts.size < 2
|
||||||
next if coord_parts.size < 2
|
|
||||||
|
|
||||||
lng, lat, alt = coord_parts.map(&:to_f)
|
lng, lat, alt = coord_parts.map(&:to_f)
|
||||||
|
|
||||||
points << {
|
points << {
|
||||||
lonlat: "POINT(#{lng} #{lat})",
|
lonlat: "POINT(#{lng} #{lat})",
|
||||||
altitude: alt&.to_i || 0,
|
altitude: alt&.to_i || 0,
|
||||||
timestamp: time,
|
timestamp: time,
|
||||||
import_id: import.id,
|
import_id: import.id,
|
||||||
velocity: 0.0,
|
velocity: 0.0,
|
||||||
raw_data: { source: 'gx_track', index: i },
|
raw_data: { source: 'gx_track', index: i },
|
||||||
user_id: user_id,
|
user_id: user_id,
|
||||||
created_at: Time.current,
|
created_at: Time.current,
|
||||||
updated_at: Time.current
|
updated_at: Time.current
|
||||||
}
|
}
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.warn("Failed to parse gx:Track point at index #{i}: #{e.message}")
|
Rails.logger.warn("Failed to parse gx:Track point at index #{i}: #{e.message}")
|
||||||
next
|
next
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
points
|
points
|
||||||
|
|
@ -133,6 +177,12 @@ class Kml::Importer
|
||||||
end.compact
|
end.compact
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def has_explicit_timestamp?(placemark)
|
||||||
|
REXML::XPath.first(placemark, './/TimeStamp/when') ||
|
||||||
|
REXML::XPath.first(placemark, './/TimeSpan/begin') ||
|
||||||
|
REXML::XPath.first(placemark, './/TimeSpan/end')
|
||||||
|
end
|
||||||
|
|
||||||
def extract_timestamp(placemark)
|
def extract_timestamp(placemark)
|
||||||
# Try TimeStamp first
|
# Try TimeStamp first
|
||||||
timestamp_node = REXML::XPath.first(placemark, './/TimeStamp/when')
|
timestamp_node = REXML::XPath.first(placemark, './/TimeStamp/when')
|
||||||
|
|
@ -146,11 +196,11 @@ class Kml::Importer
|
||||||
timespan_end = REXML::XPath.first(placemark, './/TimeSpan/end')
|
timespan_end = REXML::XPath.first(placemark, './/TimeSpan/end')
|
||||||
return Time.parse(timespan_end.text).to_i if timespan_end
|
return Time.parse(timespan_end.text).to_i if timespan_end
|
||||||
|
|
||||||
# Default to import creation time if no timestamp found
|
# No timestamp found - this should not happen if has_explicit_timestamp? was checked
|
||||||
import.created_at.to_i
|
raise 'No timestamp found in placemark'
|
||||||
rescue StandardError => e
|
rescue StandardError => e
|
||||||
Rails.logger.warn("Failed to parse timestamp: #{e.message}")
|
Rails.logger.error("Failed to parse timestamp: #{e.message}")
|
||||||
import.created_at.to_i
|
raise e
|
||||||
end
|
end
|
||||||
|
|
||||||
def build_point(coord, timestamp, placemark)
|
def build_point(coord, timestamp, placemark)
|
||||||
|
|
|
||||||
BIN
spec/fixtures/files/kml/points_with_timestamps.kmz
vendored
Normal file
BIN
spec/fixtures/files/kml/points_with_timestamps.kmz
vendored
Normal file
Binary file not shown.
|
|
@ -142,6 +142,31 @@ RSpec.describe Kml::Importer do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context 'when importing KMZ file (compressed KML)' do
|
||||||
|
let(:file_path) { Rails.root.join('spec/fixtures/files/kml/points_with_timestamps.kmz').to_s }
|
||||||
|
|
||||||
|
it 'extracts and processes KML from KMZ archive' do
|
||||||
|
expect { parser }.to change(Point, :count).by(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'creates points with correct data from extracted KML' do
|
||||||
|
parser
|
||||||
|
|
||||||
|
point = user.points.order(:timestamp).first
|
||||||
|
|
||||||
|
expect(point.lat).to eq(37.4220)
|
||||||
|
expect(point.lon).to eq(-122.0841)
|
||||||
|
expect(point.altitude).to eq(10)
|
||||||
|
expect(point.timestamp).to eq(Time.zone.parse('2024-01-15T12:00:00Z').to_i)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'broadcasts importing progress' do
|
||||||
|
expect_any_instance_of(Imports::Broadcaster).to receive(:broadcast_import_progress).at_least(1).time
|
||||||
|
|
||||||
|
parser
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when import fails' do
|
context 'when import fails' do
|
||||||
let(:file_path) { Rails.root.join('spec/fixtures/files/kml/points_with_timestamps.kml').to_s }
|
let(:file_path) { Rails.root.join('spec/fixtures/files/kml/points_with_timestamps.kml').to_s }
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue