Skip to content

Commit e9d689c

Browse files
authored
Merge pull request #386 from gtt-project/feat/geojson-precision
feat: configurable GeoJSON coordinate precision (default 6)
2 parents 6337078 + d225569 commit e9d689c

10 files changed

Lines changed: 84 additions & 15 deletions

File tree

app/views/settings/gtt/_general.html.erb

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,3 +79,11 @@
7979
<%= content_tag(:label, l(:label_hide_map_for_invalid_geom)) %>
8080
<%= check_box_tag 'settings[hide_map_for_invalid_geom]', 1, Setting.plugin_redmine_gtt['hide_map_for_invalid_geom'] %>
8181
</p>
82+
83+
<p>
84+
<%= content_tag(:label, l(:label_geojson_precision)) %>
85+
<%= number_field_tag('settings[geojson_precision]',
86+
@settings['geojson_precision'],
87+
min: 0, max: 15, step: 1) %>
88+
<em class="info"><%= l(:text_geojson_precision_info) %></em>
89+
</p>

config/locales/de.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ de:
121121
field_format: Format-Typ
122122
select_other_gtt_settings: Andere GTT-Einstellungen
123123
label_hide_map_for_invalid_geom: Ausblenden der Ausgabekarte für ungültige Geometrie
124+
label_geojson_precision: "GeoJSON-Koordinatengenauigkeit"
125+
text_geojson_precision_info: "Nachkommastellen der Koordinaten in der GeoJSON-Ausgabe (EPSG:4326). 6 entspricht etwa 0,11 m; kleinere Werte verringern die Datenmenge."
124126
gtt_hide_map_for_invalid_geom_info: Bitte bearbeiten Sie die Anfrage und stellen
125127
Sie die Geometrie ein, um die Karte zu sehen.
126128
label_default_measure_enabled: Zeige Steuerung zum Messen von Länge und Fläche

config/locales/en.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ en:
6060

6161
select_other_gtt_settings: "Other GTT settings"
6262
label_hide_map_for_invalid_geom: "Hide issue map for invalid geometry"
63+
label_geojson_precision: "GeoJSON coordinate precision"
64+
text_geojson_precision_info: "Decimal places for coordinates in GeoJSON output (EPSG:4326). 6 is about 0.11 m; lower values shrink the payload."
6365
gtt_hide_map_for_invalid_geom_info: "Please edit the issue and set the geometry
6466
to see the map."
6567

config/locales/ja.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,8 @@ ja:
5858

5959
select_other_gtt_settings: "GTTのその他の設定"
6060
label_hide_map_for_invalid_geom: "位置情報が登録されていない時に地図を隠す"
61+
label_geojson_precision: "GeoJSON座標の小数点桁数"
62+
text_geojson_precision_info: "GeoJSON出力(EPSG:4326)の座標の小数点以下の桁数。6桁で約0.11m。小さくするとデータ量を削減できます。"
6163
gtt_hide_map_for_invalid_geom_info: "地図を見るにはチケットを編集して位置情報を登録してください。"
6264

6365
select_default_tracker_icon: "トラッカーアイコンを選択:"

init.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,10 @@
3434
'editable_geometry_types_on_issue_map' => ["Point"],
3535
'enable_geojson_upload_on_issue_map' => false,
3636
'enable_geocoding_on_map' => false,
37-
'hide_map_for_invalid_geom' => false
37+
'hide_map_for_invalid_geom' => false,
38+
# Keep in sync with RedmineGtt::DEFAULT_GEOJSON_PRECISION (literal here to
39+
# avoid autoloading the module during plugin registration).
40+
'geojson_precision' => 6
3841
},
3942
partial: 'settings/gtt/settings'
4043
)

lib/redmine_gtt.rb

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,21 @@
22

33
module RedmineGtt
44

5+
# Number of decimal places GeoJSON coordinates are rounded to on output
6+
# (EPSG:4326 degrees, so 6 digits is ~0.11 m, 7 ~1 cm). RGeo's encoder emits
7+
# full float precision and upstream declines to add a precision option, so
8+
# the plugin rounds itself; the default keeps payloads small without losing
9+
# any real-world accuracy. Configurable via plugin settings; clamped so a
10+
# bad value can never break SQL or produce absurd output.
11+
DEFAULT_GEOJSON_PRECISION = 6
12+
GEOJSON_PRECISION_RANGE = (0..15)
13+
14+
def self.geojson_precision
15+
raw = Setting.plugin_redmine_gtt['geojson_precision']
16+
value = Integer(raw, exception: false) || DEFAULT_GEOJSON_PRECISION
17+
value.clamp(GEOJSON_PRECISION_RANGE.min, GEOJSON_PRECISION_RANGE.max)
18+
end
19+
520
def self.setup_normal_patches
621
RedmineGtt::Patches::IssuePatch.apply
722
RedmineGtt::Patches::IssueQueryPatch.apply

lib/redmine_gtt/conversions.rb

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,25 +10,53 @@ module RedmineGtt
1010
module Conversions
1111

1212
class GeomToJson
13-
def initialize()
13+
# RGeo's GeoJSON encoder emits full float precision and offers no
14+
# rounding option (see gtt-project/redmine_gtt#7), so coordinates are
15+
# rounded after encoding. Defaults to the configured plugin precision.
16+
def initialize(precision: RedmineGtt.geojson_precision)
1417
@factory = RGeo::GeoJSON::EntityFactory.instance
18+
@precision = precision
1519
end
1620

1721
def to_json(object, id: nil, properties: nil)
18-
RGeo::GeoJSON.encode feature(object, id, properties)
22+
round_coordinates RGeo::GeoJSON.encode(feature(object, id, properties))
1923
end
2024

2125
def collection_to_json(data)
22-
RGeo::GeoJSON.encode @factory.feature_collection(
26+
round_coordinates RGeo::GeoJSON.encode(@factory.feature_collection(
2327
data.map{|object, id, properties| feature(object, id, properties)}
24-
)
28+
))
2529
end
2630

2731
private
2832

2933
def feature(object, id, properties = nil)
3034
@factory.feature object, id, (properties || {})
3135
end
36+
37+
# Walks the encoded GeoJSON and rounds the numbers under any
38+
# "coordinates" key to @precision decimal places, leaving everything
39+
# else (notably "properties") untouched.
40+
def round_coordinates(node)
41+
case node
42+
when Hash
43+
node.each_with_object({}) do |(key, value), result|
44+
result[key] = key == 'coordinates' ? round_numbers(value) : round_coordinates(value)
45+
end
46+
when Array
47+
node.map { |element| round_coordinates(element) }
48+
else
49+
node
50+
end
51+
end
52+
53+
def round_numbers(value)
54+
case value
55+
when Array then value.map { |element| round_numbers(element) }
56+
when Numeric then value.round(@precision)
57+
else value
58+
end
59+
end
3260
end
3361

3462
class WkbToGeom

lib/redmine_gtt/patches/geojson_attribute.rb

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ def self.prepended(base)
2424
module ClassMethods
2525

2626
def geojson_attribute_select
27-
"ST_AsGeoJson(#{table_name}.geom) as db_geojson"
27+
# maxdecimaldigits keeps coordinate precision (and payload size) in
28+
# check; RedmineGtt.geojson_precision returns a clamped Integer so
29+
# the interpolation is injection-safe.
30+
"ST_AsGeoJson(#{table_name}.geom, #{RedmineGtt.geojson_precision}) as db_geojson"
2831
end
2932

3033
def array_to_geojson(array, include_properties: false)

test/test_helper.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,13 @@ def assert_geojson(json)
4444
end
4545

4646
def assert_equal_coordinates(a, b)
47-
assert_equal a.flatten.map{|f|f.round 5}, b.flatten.map{|f|f.round 5}
47+
# Compare at the precision GeoJSON output is actually rounded to (follows
48+
# the configured setting, so it stays correct if a test changes it).
49+
# Comparing at a lower precision than the output uses would double-round
50+
# boundary values (e.g. 135.2528349 -> round(6) 135.252835 -> round(5)
51+
# 135.25284, while a direct round(5) gives 135.25283).
52+
precision = RedmineGtt.geojson_precision
53+
assert_equal a.flatten.map{|f|f.round precision}, b.flatten.map{|f|f.round precision}
4854
end
4955

5056
def assert_geojson_collection(json)

test/unit/issue_test.rb

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@ class IssueTest < GttTest
7474
new_coordinates = old_coordinates.map{|c| c + 0.000000001}
7575
@issue.update_attribute :geojson, point_geojson(new_coordinates)
7676
@issue.instance_variable_set "@geojson", nil
77-
assert_equal old_coordinates, @issue.geojson["geometry"]["coordinates"]
77+
assert_equal_coordinates old_coordinates, @issue.geojson["geometry"]["coordinates"]
7878

7979
new_coordinates = [old_coordinates[0] + 0.2, old_coordinates[1], old_coordinates[2]]
8080
@issue.update_attribute :geojson, point_geojson(new_coordinates)
8181
@issue.instance_variable_set "@geojson", nil
82-
assert_equal new_coordinates, @issue.geojson["geometry"]["coordinates"]
82+
assert_equal_coordinates new_coordinates, @issue.geojson["geometry"]["coordinates"]
8383
end
8484

8585
test 'should ignore small linestring geom changes' do
@@ -92,18 +92,18 @@ class IssueTest < GttTest
9292
new_coordinates = old_coordinates.map{|c| [c[0] + 0.000000001, c[1] + 0.000000001, c[2]]}
9393
@issue.update_attribute :geojson, linestring_geojson(new_coordinates)
9494
@issue.instance_variable_set "@geojson", nil
95-
assert_equal old_coordinates, @issue.geojson["geometry"]["coordinates"]
95+
assert_equal_coordinates old_coordinates, @issue.geojson["geometry"]["coordinates"]
9696

9797
new_coordinates = old_coordinates.map{|c| [c[0] + 0.2, c[1], c[2]]}
9898
@issue.update_attribute :geojson, linestring_geojson(new_coordinates)
9999
@issue.instance_variable_set "@geojson", nil
100-
assert_equal new_coordinates, @issue.geojson["geometry"]["coordinates"]
100+
assert_equal_coordinates new_coordinates, @issue.geojson["geometry"]["coordinates"]
101101

102102
new_coordinates = old_coordinates.map{|c| [c[0], c[1], c[2]]}
103103
new_coordinates.delete_at(1)
104104
@issue.update_attribute :geojson, linestring_geojson(new_coordinates)
105105
@issue.instance_variable_set "@geojson", nil
106-
assert_equal new_coordinates, @issue.geojson["geometry"]["coordinates"]
106+
assert_equal_coordinates new_coordinates, @issue.geojson["geometry"]["coordinates"]
107107
end
108108

109109
test 'should ignore small polygon geom changes' do
@@ -116,17 +116,17 @@ class IssueTest < GttTest
116116
new_coordinates = [old_coordinates[0].map{|c| [c[0] + 0.000000001, c[1] + 0.000000001, c[2]]}]
117117
@issue.update_attribute :geojson, polygon_geojson(new_coordinates)
118118
@issue.instance_variable_set "@geojson", nil
119-
assert_equal old_coordinates, @issue.geojson["geometry"]["coordinates"]
119+
assert_equal_coordinates old_coordinates, @issue.geojson["geometry"]["coordinates"]
120120

121121
new_coordinates = [old_coordinates[0].map{|c| [c[0] + 0.2, c[1], c[2]]}]
122122
@issue.update_attribute :geojson, polygon_geojson(new_coordinates)
123123
@issue.instance_variable_set "@geojson", nil
124-
assert_equal new_coordinates, @issue.geojson["geometry"]["coordinates"]
124+
assert_equal_coordinates new_coordinates, @issue.geojson["geometry"]["coordinates"]
125125

126126
new_coordinates = [old_coordinates[0].map{|c| [c[0], c[1], c[2]]}]
127127
new_coordinates[0].insert(2, [135.301041779,34.680969984,0.0])
128128
@issue.update_attribute :geojson, polygon_geojson(new_coordinates)
129129
@issue.instance_variable_set "@geojson", nil
130-
assert_equal new_coordinates, @issue.geojson["geometry"]["coordinates"]
130+
assert_equal_coordinates new_coordinates, @issue.geojson["geometry"]["coordinates"]
131131
end
132132
end

0 commit comments

Comments
 (0)