Skip to content

Commit 5a019b7

Browse files
authored
Merge pull request #411 from gtt-project/feat/distance-unit-setting
feat: configurable distance display unit
2 parents 960c845 + cb2a13d commit 5a019b7

11 files changed

Lines changed: 260 additions & 2 deletions

File tree

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

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,4 +88,31 @@
8888
min: 0, max: 15, step: 1) %>
8989
<em class="info"><%= l(:text_geojson_precision_info) %></em>
9090
</p>
91+
92+
<p>
93+
<%= content_tag(:label, l(:label_distance_unit)) %>
94+
<%= select_tag 'settings[distance_unit]',
95+
options_for_select(
96+
RedmineGtt::DistanceUnit::METERS_PER_UNIT.keys.map { |u|
97+
[l(:"gtt_distance_unit_#{u}"), u]
98+
},
99+
RedmineGtt::DistanceUnit.current
100+
) %>
101+
<em class="info"><%= l(:text_distance_unit_info) %></em>
102+
</p>
103+
104+
<p>
105+
<%= content_tag(:label, l(:label_api_distance_unit)) %>
106+
<%# reserved: the API contract is meters-only for now; the setting
107+
exists so a later version can offer more units without a
108+
settings migration %>
109+
<%= select_tag 'settings[api_distance_unit]',
110+
options_for_select(
111+
RedmineGtt::DistanceUnit::API_UNITS.map { |u|
112+
[l(:"gtt_distance_unit_#{u}"), u]
113+
},
114+
RedmineGtt::DistanceUnit.api_unit
115+
) %>
116+
<em class="info"><%= l(:text_api_distance_unit_info) %></em>
117+
</p>
91118
</fieldset>

config/locales/de.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,15 @@ de:
130130
gtt_hide_map_for_invalid_geom_info: Bitte bearbeiten Sie die Anfrage und stellen
131131
Sie die Geometrie ein, um die Karte zu sehen.
132132
label_default_measure_enabled: Zeige Steuerung zum Messen von Länge und Fläche
133+
label_distance_unit: "Anzeigeeinheit für Entfernungen"
134+
text_distance_unit_info: "Einheit für angezeigte und eingegebene Entfernungen (Distanzfilter, Distanzspalte, Beobachtungsradius)."
135+
label_api_distance_unit: "API-Entfernungseinheit"
136+
text_api_distance_unit_info: "Die REST-API verwendet in dieser Version immer Meter."
137+
gtt_distance_unit_m: "Meter (m)"
138+
gtt_distance_unit_km: "Kilometer (km)"
139+
gtt_distance_unit_ft: "Fuß (ft)"
140+
gtt_distance_unit_mi: "Meilen (mi)"
141+
gtt_distance_unit_nm: "Seemeilen (nm)"
133142
gtt_label_watch_nearby_legend: "Automatisch beobachten: Tickets in der Nähe"
134143
gtt_label_watch_nearby: "Neue Tickets in der Nähe meines Standorts beobachten"
135144
gtt_label_watch_nearby_radius: "Entfernung"

config/locales/en.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,15 @@ en:
6464
text_geojson_precision_info: "Decimal places for coordinates in GeoJSON output (EPSG:4326). 6 is about 0.11 m; lower values shrink the payload."
6565
gtt_hide_map_for_invalid_geom_info: "Please edit the issue and set the geometry
6666
to see the map."
67+
label_distance_unit: "Distance display unit"
68+
text_distance_unit_info: "Unit for distances shown and entered in the UI (distance filter, distance column, watch radius)."
69+
label_api_distance_unit: "API distance unit"
70+
text_api_distance_unit_info: "The REST API always uses meters in this version."
71+
gtt_distance_unit_m: "Meters (m)"
72+
gtt_distance_unit_km: "Kilometers (km)"
73+
gtt_distance_unit_ft: "Feet (ft)"
74+
gtt_distance_unit_mi: "Miles (mi)"
75+
gtt_distance_unit_nm: "Nautical miles (nm)"
6776

6877
gtt_label_watch_nearby_legend: "Auto watch: nearby issues"
6978
gtt_label_watch_nearby: "Watch new issues near my location"

config/locales/ja.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,15 @@ ja:
6161
label_geojson_precision: "GeoJSON座標の小数点桁数"
6262
text_geojson_precision_info: "GeoJSON出力(EPSG:4326)の座標の小数点以下の桁数。6桁で約0.11m。小さくするとデータ量を削減できます。"
6363
gtt_hide_map_for_invalid_geom_info: "地図を見るにはチケットを編集して位置情報を登録してください。"
64+
label_distance_unit: "距離の表示単位"
65+
text_distance_unit_info: "画面で表示・入力する距離の単位(距離フィルタ、距離カラム、ウォッチ半径)。"
66+
label_api_distance_unit: "APIの距離単位"
67+
text_api_distance_unit_info: "このバージョンではREST APIは常にメートルを使用します。"
68+
gtt_distance_unit_m: "メートル (m)"
69+
gtt_distance_unit_km: "キロメートル (km)"
70+
gtt_distance_unit_ft: "フィート (ft)"
71+
gtt_distance_unit_mi: "マイル (mi)"
72+
gtt_distance_unit_nm: "海里 (nm)"
6473

6574
gtt_label_watch_nearby_legend: "自動ウォッチ: 近くのチケット"
6675
gtt_label_watch_nearby: "登録した所在地の近くの新しいチケットをウォッチする"

init.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@
4545
'hide_map_for_invalid_geom' => false,
4646
# Keep in sync with RedmineGtt::DEFAULT_GEOJSON_PRECISION (literal here to
4747
# avoid autoloading the module during plugin registration).
48-
'geojson_precision' => 6
48+
'geojson_precision' => 6,
49+
'distance_unit' => 'm',
50+
'api_distance_unit' => 'm'
4951
},
5052
partial: 'settings/gtt/settings'
5153
)

lib/redmine_gtt.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ def self.setup_controller_patches
5353
RedmineGtt::Patches::ProjectsControllerPatch.apply
5454
RedmineGtt::Patches::UsersControllerPatch.apply
5555
RedmineGtt::Patches::IssuesHelperPatch.apply
56+
RedmineGtt::Patches::QueriesHelperPatch.apply
5657

5758
[
5859
IssuesController,

lib/redmine_gtt/distance_unit.rb

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# frozen_string_literal: true
2+
3+
module RedmineGtt
4+
# Converts between meters and the display unit configured in the plugin
5+
# settings (#10). Meters are the internal unit everywhere: storage, SQL,
6+
# the filter wire format and the REST API all stay metric; only what the
7+
# user sees and types is converted.
8+
module DistanceUnit
9+
10+
METERS_PER_UNIT = {
11+
'm' => 1.0,
12+
'km' => 1000.0,
13+
'ft' => 0.3048,
14+
'mi' => 1609.344,
15+
'nm' => 1852.0
16+
}.freeze
17+
18+
DEFAULT = 'm'
19+
20+
# Units the REST API may use. Reserved for future flexibility: the API
21+
# contract is meters and only meters for now, but the setting exists so
22+
# a later version can widen this list without a settings migration.
23+
API_UNITS = %w(m).freeze
24+
25+
# The configured display unit, falling back to meters for anything
26+
# unknown (empty setting, typo from a manually edited setting hash).
27+
def self.current
28+
unit = Setting.plugin_redmine_gtt['distance_unit'].to_s
29+
METERS_PER_UNIT.key?(unit) ? unit : DEFAULT
30+
end
31+
32+
# The unit used by the REST API. Clamped to meters regardless of the
33+
# stored value while API_UNITS has a single entry.
34+
def self.api_unit
35+
unit = Setting.plugin_redmine_gtt['api_distance_unit'].to_s
36+
API_UNITS.include?(unit) ? unit : DEFAULT
37+
end
38+
39+
# Unknown units fall back to meters instead of raising: the setting can
40+
# be edited by hand and callers may pass user-supplied unit strings.
41+
def self.from_meters(meters, unit = current)
42+
meters.to_f / METERS_PER_UNIT.fetch(unit, 1.0)
43+
end
44+
45+
def self.to_meters(value, unit = current)
46+
value.to_f * METERS_PER_UNIT.fetch(unit, 1.0)
47+
end
48+
49+
end
50+
end

lib/redmine_gtt/patches/issue_query_patch.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,8 @@ def available_columns
4343
caption: :field_geom
4444
)
4545
columns << QueryColumn.new(:distance,
46-
caption: :label_gtt_distance,
46+
# header shows the configured display unit, e.g. "Distance (km)"
47+
caption: -> { "#{l(:label_gtt_distance)} (#{DistanceUnit.current})" },
4748
sortable: lambda{
4849
lng, lat = find_center_point
4950
distance_query lng, lat
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
module RedmineGtt
2+
module Patches
3+
4+
# Renders the distance column in the configured display unit (#10).
5+
# The underlying value stays meters (it is what the query selects and
6+
# what the REST API returns); only the rendered list/CSV cell converts.
7+
module QueriesHelperPatch
8+
9+
def self.apply
10+
QueriesHelper.prepend self unless QueriesHelper < self
11+
end
12+
13+
def column_value(column, item, value)
14+
if column.name == :distance && value.present?
15+
gtt_format_distance(value)
16+
else
17+
super
18+
end
19+
end
20+
21+
def csv_value(column, object, value)
22+
if column.name == :distance && value.present?
23+
# mirror core's CSV float formatting incl. the decimal separator
24+
gtt_format_distance(value).gsub('.', l(:general_csv_decimal_separator))
25+
else
26+
super
27+
end
28+
end
29+
30+
private
31+
32+
def gtt_format_distance(meters)
33+
sprintf('%.2f', RedmineGtt::DistanceUnit.from_meters(meters))
34+
end
35+
36+
end
37+
end
38+
end

test/unit/distance_unit_test.rb

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
require_relative '../test_helper'
2+
3+
class DistanceUnitTest < GttTest
4+
5+
teardown do
6+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
7+
'distance_unit' => 'm', 'api_distance_unit' => 'm'
8+
)
9+
end
10+
11+
test 'defaults to meters' do
12+
assert_equal 'm', RedmineGtt::DistanceUnit.current
13+
assert_equal 'm', RedmineGtt::DistanceUnit.api_unit
14+
end
15+
16+
test 'falls back to meters for unknown units' do
17+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
18+
'distance_unit' => 'parsec'
19+
)
20+
assert_equal 'm', RedmineGtt::DistanceUnit.current
21+
end
22+
23+
test 'reads the configured display unit' do
24+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
25+
'distance_unit' => 'km'
26+
)
27+
assert_equal 'km', RedmineGtt::DistanceUnit.current
28+
end
29+
30+
test 'api unit is clamped to meters regardless of the stored value' do
31+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
32+
'api_distance_unit' => 'km'
33+
)
34+
assert_equal 'm', RedmineGtt::DistanceUnit.api_unit
35+
end
36+
37+
test 'converts from meters into the given unit' do
38+
assert_equal 1.5, RedmineGtt::DistanceUnit.from_meters(1500, 'km')
39+
assert_in_delta 0.932, RedmineGtt::DistanceUnit.from_meters(1500, 'mi'), 0.001
40+
assert_in_delta 4921.26, RedmineGtt::DistanceUnit.from_meters(1500, 'ft'), 0.01
41+
assert_in_delta 0.81, RedmineGtt::DistanceUnit.from_meters(1500, 'nm'), 0.001
42+
assert_equal 1500.0, RedmineGtt::DistanceUnit.from_meters(1500, 'm')
43+
end
44+
45+
test 'converts a unit value into meters' do
46+
assert_equal 1500.0, RedmineGtt::DistanceUnit.to_meters(1.5, 'km')
47+
assert_in_delta 1609.344, RedmineGtt::DistanceUnit.to_meters(1, 'mi'), 0.001
48+
assert_equal 25.0, RedmineGtt::DistanceUnit.to_meters(25, 'm')
49+
end
50+
51+
test 'conversions treat unknown units as meters instead of raising' do
52+
assert_equal 42.0, RedmineGtt::DistanceUnit.from_meters(42, 'parsec')
53+
assert_equal 42.0, RedmineGtt::DistanceUnit.to_meters(42, 'parsec')
54+
end
55+
56+
test 'conversions honor the configured unit by default' do
57+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
58+
'distance_unit' => 'km'
59+
)
60+
assert_equal 2.0, RedmineGtt::DistanceUnit.from_meters(2000)
61+
assert_equal 2000.0, RedmineGtt::DistanceUnit.to_meters(2)
62+
end
63+
64+
test 'distance column caption carries the unit' do
65+
Setting.plugin_redmine_gtt = Setting.plugin_redmine_gtt.merge(
66+
'distance_unit' => 'mi'
67+
)
68+
column = IssueQuery.new.available_columns.detect { |c| c.name == :distance }
69+
assert column, 'distance column should be available on global queries'
70+
assert_match(/\(mi\)\z/, column.caption)
71+
end
72+
end

0 commit comments

Comments
 (0)