gpx_doctor 0.2.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c1cba3a635e28b6b5731f9a178342323f6068e3954e03439c4c908045346d936
4
- data.tar.gz: 0a9dd34154ee4c75abd3c9556bf69fb80e238ca0a4d940d9371607f299871b73
3
+ metadata.gz: 367e09fc58af88bd756703fd5e2647632fd9991f753f3581e3bcb439f7733ce3
4
+ data.tar.gz: 3a7922964bb774be1eb1a1d2af78aee4a7d56b868b7652c481fb186cc6438bce
5
5
  SHA512:
6
- metadata.gz: e9981e204bdf04c176e500920be0ff75b14a011e50d3a149590545adf4351d6e5f00e37ab51bb998f43b97d2619afa0927accb606ab584eab84f0d4e29d2229e
7
- data.tar.gz: 4b226863088aac9b8af344985426bc4beeee6693f1772b710537edb7e6c7059623f0824621f20f40cee55fe13114af7acdd306c524f51cd1fcdec38e50b44fb9
6
+ metadata.gz: 9bad3d27880e55e001fea734b7d481f19569f0e0d202958d4ae893a1722eef5b0fb7a5431030f970872358c812e5a83a492f99c945096652e9547f0945238624
7
+ data.tar.gz: 0a4a032459ed9916bc8cd99c64d38676822efbed6e1a470d86a9806878f3ee65f5239827f1830eeb6dbdac573f845a64bcea70b7f24f5b686bc8ef440fb29325
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module GpxDoctor
4
+ class Error < StandardError; end
5
+ class InvalidGpxError < Error; end
6
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module GpxDoctor
6
+ # Builds GeoJSON output from parsed GPX data according to RFC 7946.
7
+ #
8
+ # RFC 7946 compliance:
9
+ # - Coordinate order: [longitude, latitude, elevation]
10
+ # - LineString geometries require minimum 2 positions
11
+ # - MultiLineString segments require minimum 2 positions each
12
+ # - Empty or invalid geometries are filtered out
13
+ # - Properties member is null when no properties exist
14
+ class GeoJsonBuilder
15
+ class << self
16
+ def build(result)
17
+ new(result).build
18
+ end
19
+
20
+ def build_file(result, file_path)
21
+ json = build(result)
22
+ File.write(file_path, json)
23
+ json
24
+ end
25
+ end
26
+
27
+ def initialize(result)
28
+ @result = result
29
+ end
30
+
31
+ def build
32
+ features = []
33
+ features.concat(waypoint_features)
34
+ features.concat(route_features)
35
+ features.concat(track_features)
36
+
37
+ JSON.generate(
38
+ type: 'FeatureCollection',
39
+ features: features
40
+ )
41
+ end
42
+
43
+ private
44
+
45
+ def waypoint_features
46
+ @result.waypoints.map { |wpt| point_feature(wpt) }
47
+ end
48
+
49
+ def route_features
50
+ @result.routes.map do |route|
51
+ coords = route.points.map { |pt| coordinate(pt) }
52
+ # RFC 7946: LineString must have 2 or more positions
53
+ next if coords.length < 2
54
+
55
+ props = route_properties(route)
56
+ {
57
+ type: 'Feature',
58
+ properties: props.empty? ? nil : props,
59
+ geometry: {
60
+ type: 'LineString',
61
+ coordinates: coords
62
+ }
63
+ }
64
+ end.compact
65
+ end
66
+
67
+ def track_features
68
+ @result.tracks.map do |track|
69
+ # RFC 7946: Each LineString in MultiLineString must have 2+ positions
70
+ coords = track.segments.map do |seg|
71
+ seg_coords = seg.points.map { |pt| coordinate(pt) }
72
+ seg_coords if seg_coords.length >= 2
73
+ end.compact
74
+
75
+ # Skip tracks with no valid segments
76
+ next if coords.empty?
77
+
78
+ props = track_properties(track)
79
+ {
80
+ type: 'Feature',
81
+ properties: props.empty? ? nil : props,
82
+ geometry: {
83
+ type: 'MultiLineString',
84
+ coordinates: coords
85
+ }
86
+ }
87
+ end.compact
88
+ end
89
+
90
+ def point_feature(wpt)
91
+ props = waypoint_properties(wpt)
92
+ {
93
+ type: 'Feature',
94
+ properties: props.empty? ? nil : props,
95
+ geometry: {
96
+ type: 'Point',
97
+ coordinates: coordinate(wpt)
98
+ }
99
+ }
100
+ end
101
+
102
+ def coordinate(point)
103
+ coord = [point.lon, point.lat]
104
+ coord << point.ele if point.ele
105
+ coord
106
+ end
107
+
108
+ def waypoint_properties(wpt)
109
+ props = {}
110
+ props[:name] = wpt.name if wpt.name
111
+ props[:desc] = wpt.desc if wpt.desc
112
+ props[:sym] = wpt.sym if wpt.sym
113
+ props[:type] = wpt.type if wpt.type
114
+ props[:time] = wpt.time.iso8601 if wpt.time
115
+ props
116
+ end
117
+
118
+ def route_properties(route)
119
+ props = {}
120
+ props[:name] = route.name if route.name
121
+ props[:desc] = route.desc if route.desc
122
+ props[:type] = route.type if route.type
123
+ props[:number] = route.number if route.number
124
+ props
125
+ end
126
+
127
+ def track_properties(track)
128
+ props = {}
129
+ props[:name] = track.name if track.name
130
+ props[:desc] = track.desc if track.desc
131
+ props[:type] = track.type if track.type
132
+ props[:number] = track.number if track.number
133
+ props
134
+ end
135
+ end
136
+ end
@@ -20,7 +20,8 @@ module GpxDoctor
20
20
  end
21
21
 
22
22
  def parse_string(xml_string, params: {})
23
- doc = Nokogiri::XML(xml_string)
23
+ Validator.validate!(xml_string)
24
+ doc = Nokogiri::XML(xml_string) { |config| config.nonet }
24
25
  ns = detect_namespace(doc)
25
26
 
26
27
  new(doc, ns, params: params).parse
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module GpxDoctor
6
+ class Validator
7
+ ALLOWED_VERSIONS = %w[1.0 1.1].freeze
8
+
9
+ # Matches a DOCTYPE declaration anywhere in the string (case-insensitive).
10
+ # DOCTYPE is rejected entirely because it can carry external-entity
11
+ # references (XXE) or internal entity bombs (billion-laughs).
12
+ DOCTYPE_PATTERN = /<!DOCTYPE/i.freeze
13
+
14
+ class << self
15
+ def validate!(xml_string)
16
+ new(xml_string).validate!
17
+ end
18
+ end
19
+
20
+ def initialize(xml_string)
21
+ @xml_string = xml_string
22
+ end
23
+
24
+ # Raises GpxDoctor::InvalidGpxError if the input is not a valid, safe GPX document.
25
+ def validate!
26
+ reject_doctype!
27
+ doc = parse_xml!
28
+ validate_root!(doc)
29
+ validate_version!(doc)
30
+ true
31
+ end
32
+
33
+ private
34
+
35
+ def reject_doctype!
36
+ return unless @xml_string.match?(DOCTYPE_PATTERN)
37
+
38
+ raise InvalidGpxError, 'DOCTYPE declarations are not allowed'
39
+ end
40
+
41
+ def parse_xml!
42
+ doc = Nokogiri::XML(@xml_string) { |config| config.nonet }
43
+ return doc if doc.errors.empty?
44
+
45
+ raise InvalidGpxError, "Invalid XML: #{doc.errors.map(&:message).join('; ')}"
46
+ end
47
+
48
+ def validate_root!(doc)
49
+ root = doc.root
50
+ return if root&.name == 'gpx'
51
+
52
+ raise InvalidGpxError, 'Root element must be <gpx>'
53
+ end
54
+
55
+ def validate_version!(doc)
56
+ version = doc.root['version']
57
+ return if ALLOWED_VERSIONS.include?(version)
58
+
59
+ raise InvalidGpxError,
60
+ "Unsupported GPX version: #{version.inspect}. Supported versions: #{ALLOWED_VERSIONS.join(', ')}"
61
+ end
62
+ end
63
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module GpxDoctor
4
- VERSION = '0.2.0'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/gpx_doctor.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'gpx_doctor/version'
4
+ require 'gpx_doctor/errors'
4
5
  require 'gpx_doctor/configuration'
6
+ require 'gpx_doctor/validator'
5
7
  require 'gpx_doctor/models/email'
6
8
  require 'gpx_doctor/models/link'
7
9
  require 'gpx_doctor/models/copyright'
@@ -19,6 +21,7 @@ require 'gpx_doctor/segment_splitter'
19
21
  require 'gpx_doctor/point_selector'
20
22
  require 'gpx_doctor/parser'
21
23
  require 'gpx_doctor/builder'
24
+ require 'gpx_doctor/geojson_builder'
22
25
 
23
26
  module GpxDoctor
24
27
  class << self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gpx_doctor
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Poltrax
@@ -49,6 +49,8 @@ files:
49
49
  - lib/gpx_doctor/configuration.rb
50
50
  - lib/gpx_doctor/distance_calculator.rb
51
51
  - lib/gpx_doctor/elevation_client.rb
52
+ - lib/gpx_doctor/errors.rb
53
+ - lib/gpx_doctor/geojson_builder.rb
52
54
  - lib/gpx_doctor/models/bounds.rb
53
55
  - lib/gpx_doctor/models/copyright.rb
54
56
  - lib/gpx_doctor/models/email.rb
@@ -63,6 +65,7 @@ files:
63
65
  - lib/gpx_doctor/point_selector.rb
64
66
  - lib/gpx_doctor/segment_splitter.rb
65
67
  - lib/gpx_doctor/statistics_enhancer.rb
68
+ - lib/gpx_doctor/validator.rb
66
69
  - lib/gpx_doctor/version.rb
67
70
  homepage: https://github.com/Poltrax-live/gpx-doctor
68
71
  licenses: