mirror of
https://github.com/Freika/dawarich.git
synced 2026-01-10 01:01: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
|
||||
|
||||
- Cities visited during a trip are now being calculated correctly. #547 #641
|
||||
- Points on the map are now show time in user's timezone. #580 #1035
|
||||
- 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 #1682
|
||||
- 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
|
||||
- Importing KML files now creates points with correct timestamps. #1988
|
||||
- Importing KMZ files now works correctly.
|
||||
|
||||
|
||||
# [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]');
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
file_content
|
||||
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?('<kml')
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'rexml/document'
|
||||
require 'zip'
|
||||
|
||||
class Kml::Importer
|
||||
include Imports::Broadcaster
|
||||
|
|
@ -15,7 +16,7 @@ class Kml::Importer
|
|||
end
|
||||
|
||||
def call
|
||||
file_content = load_file_content
|
||||
file_content = load_kml_content
|
||||
doc = REXML::Document.new(file_content)
|
||||
|
||||
points_data = []
|
||||
|
|
@ -42,8 +43,54 @@ class Kml::Importer
|
|||
|
||||
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)
|
||||
points = []
|
||||
|
||||
return points unless has_explicit_timestamp?(placemark)
|
||||
|
||||
timestamp = extract_timestamp(placemark)
|
||||
|
||||
# Handle Point geometry
|
||||
|
|
@ -74,7 +121,6 @@ class Kml::Importer
|
|||
end
|
||||
|
||||
def parse_gx_track(track)
|
||||
# Google Earth Track extension with coordinated when/coord pairs
|
||||
points = []
|
||||
|
||||
timestamps = []
|
||||
|
|
@ -89,28 +135,26 @@ class Kml::Importer
|
|||
|
||||
# Match timestamps with coordinates
|
||||
[timestamps.size, coordinates.size].min.times do |i|
|
||||
begin
|
||||
time = Time.parse(timestamps[i]).to_i
|
||||
coord_parts = coordinates[i].split(/\s+/)
|
||||
next if coord_parts.size < 2
|
||||
time = Time.parse(timestamps[i]).to_i
|
||||
coord_parts = coordinates[i].split(/\s+/)
|
||||
next if coord_parts.size < 2
|
||||
|
||||
lng, lat, alt = coord_parts.map(&:to_f)
|
||||
lng, lat, alt = coord_parts.map(&:to_f)
|
||||
|
||||
points << {
|
||||
lonlat: "POINT(#{lng} #{lat})",
|
||||
altitude: alt&.to_i || 0,
|
||||
timestamp: time,
|
||||
import_id: import.id,
|
||||
velocity: 0.0,
|
||||
raw_data: { source: 'gx_track', index: i },
|
||||
user_id: user_id,
|
||||
created_at: Time.current,
|
||||
updated_at: Time.current
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Failed to parse gx:Track point at index #{i}: #{e.message}")
|
||||
next
|
||||
end
|
||||
points << {
|
||||
lonlat: "POINT(#{lng} #{lat})",
|
||||
altitude: alt&.to_i || 0,
|
||||
timestamp: time,
|
||||
import_id: import.id,
|
||||
velocity: 0.0,
|
||||
raw_data: { source: 'gx_track', index: i },
|
||||
user_id: user_id,
|
||||
created_at: Time.current,
|
||||
updated_at: Time.current
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Failed to parse gx:Track point at index #{i}: #{e.message}")
|
||||
next
|
||||
end
|
||||
|
||||
points
|
||||
|
|
@ -133,6 +177,12 @@ class Kml::Importer
|
|||
end.compact
|
||||
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)
|
||||
# Try TimeStamp first
|
||||
timestamp_node = REXML::XPath.first(placemark, './/TimeStamp/when')
|
||||
|
|
@ -146,11 +196,11 @@ class Kml::Importer
|
|||
timespan_end = REXML::XPath.first(placemark, './/TimeSpan/end')
|
||||
return Time.parse(timespan_end.text).to_i if timespan_end
|
||||
|
||||
# Default to import creation time if no timestamp found
|
||||
import.created_at.to_i
|
||||
# No timestamp found - this should not happen if has_explicit_timestamp? was checked
|
||||
raise 'No timestamp found in placemark'
|
||||
rescue StandardError => e
|
||||
Rails.logger.warn("Failed to parse timestamp: #{e.message}")
|
||||
import.created_at.to_i
|
||||
Rails.logger.error("Failed to parse timestamp: #{e.message}")
|
||||
raise e
|
||||
end
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
let(:file_path) { Rails.root.join('spec/fixtures/files/kml/points_with_timestamps.kml').to_s }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue