libgd-gis 0.2.9 → 0.3.1

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.
data/lib/gd/gis/map.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative "basemap"
2
4
  require_relative "projection"
3
5
  require_relative "classifier"
@@ -6,17 +8,58 @@ require_relative "layer_points"
6
8
  require_relative "layer_lines"
7
9
  require_relative "layer_polygons"
8
10
 
11
+ LINE_GEOMS = %w[LineString MultiLineString].freeze
12
+ POLY_GEOMS = %w[Polygon MultiPolygon].freeze
13
+
9
14
  module GD
10
15
  module GIS
11
- attr_accessor :debug
12
-
16
+ # Represents a complete renderable map.
17
+ #
18
+ # A Map is responsible for:
19
+ # - Managing the geographic extent (bounding box + zoom)
20
+ # - Fetching and compositing basemap tiles
21
+ # - Managing semantic feature layers
22
+ # - Managing generic overlay layers (points, lines, polygons)
23
+ # - Executing the rendering pipeline
24
+ #
25
+ # The map can render either:
26
+ # - A full tile-based image, or
27
+ # - A fixed-size viewport clipped from the basemap
28
+ #
13
29
  class Map
30
+ # Tile size in pixels (Web Mercator standard)
14
31
  TILE_SIZE = 256
15
32
 
33
+ # @return [GD::Image, nil] rendered image
16
34
  attr_reader :image
35
+
36
+ # @return [Hash<Symbol, Array>] semantic feature layers
17
37
  attr_reader :layers
38
+
39
+ # @return [Object, nil] style object
18
40
  attr_accessor :style
19
41
 
42
+ # @return [Boolean] enables debug rendering
43
+ attr_reader :debug
44
+
45
+ # Creates a new map.
46
+ #
47
+ # @param bbox [Array<Float>]
48
+ # bounding box [min_lng, min_lat, max_lng, max_lat]
49
+ # @param zoom [Integer]
50
+ # zoom level
51
+ # @param basemap [Symbol]
52
+ # basemap provider identifier
53
+ # @param width [Integer, nil]
54
+ # viewport width in pixels
55
+ # @param height [Integer, nil]
56
+ # viewport height in pixels
57
+ # @param crs [String, Symbol, nil]
58
+ # input CRS identifier
59
+ # @param fitted_bbox [Boolean]
60
+ # whether the provided bbox is already viewport-fitted
61
+ #
62
+ # @raise [ArgumentError] if parameters are invalid
20
63
  def initialize(
21
64
  bbox:,
22
65
  zoom:,
@@ -26,25 +69,19 @@ module GD
26
69
  crs: nil,
27
70
  fitted_bbox: false
28
71
  )
29
- # --------------------------------------------------
30
72
  # 1. Basic input validation
31
- # --------------------------------------------------
32
73
  raise ArgumentError, "bbox must be [min_lng, min_lat, max_lng, max_lat]" unless
33
74
  bbox.is_a?(Array) && bbox.size == 4
34
75
 
35
76
  raise ArgumentError, "zoom must be an Integer" unless zoom.is_a?(Integer)
36
77
 
37
- if (width && !height) || (!width && height)
38
- raise ArgumentError, "width and height must be provided together"
39
- end
78
+ raise ArgumentError, "width and height must be provided together" if (width && !height) || (!width && height)
40
79
 
41
80
  @zoom = zoom
42
81
  @width = width
43
82
  @height = height
44
83
 
45
- # --------------------------------------------------
46
84
  # 2. CRS normalization (input → WGS84 lon/lat)
47
- # --------------------------------------------------
48
85
  if crs
49
86
  normalizer = GD::GIS::CRS::Normalizer.new(crs)
50
87
 
@@ -54,9 +91,7 @@ module GD
54
91
  bbox = [min_lng, min_lat, max_lng, max_lat]
55
92
  end
56
93
 
57
- # --------------------------------------------------
58
94
  # 3. Final bbox (viewport-aware if width/height)
59
- # --------------------------------------------------
60
95
  @bbox =
61
96
  if width && height && !fitted_bbox
62
97
  GD::GIS::Geometry.viewport_bbox(
@@ -69,19 +104,16 @@ module GD
69
104
  bbox
70
105
  end
71
106
 
72
- # --------------------------------------------------
73
107
  # 4. Basemap (uses FINAL bbox)
74
- # --------------------------------------------------
75
108
  @basemap = GD::GIS::Basemap.new(zoom, @bbox, basemap)
76
109
 
77
- # --------------------------------------------------
78
110
  # 5. Legacy semantic layers (REQUIRED by render)
79
- # --------------------------------------------------
80
111
  @layers = {
81
112
  motorway: [],
82
113
  primary: [],
83
114
  secondary: [],
84
115
  street: [],
116
+ track: [],
85
117
  minor: [],
86
118
  rail: [],
87
119
  water: [],
@@ -91,21 +123,22 @@ module GD
91
123
  # Optional alias (semantic clarity, no behavior change)
92
124
  @road_layers = @layers
93
125
 
94
- # --------------------------------------------------
95
126
  # 6. Overlay layers (generic)
96
- # --------------------------------------------------
97
127
  @points_layers = []
98
128
  @lines_layers = []
99
129
  @polygons_layers = []
100
130
 
101
- # --------------------------------------------------
102
131
  # 7. Style
103
- # --------------------------------------------------
104
132
  @style = nil
105
133
 
106
- @debug = false
134
+ @debug = false
135
+ @used_labels = {}
107
136
  end
108
137
 
138
+ # Returns all features belonging to a given semantic layer.
139
+ #
140
+ # @param layer [Symbol]
141
+ # @return [Array<Feature>]
109
142
  def features_by_layer(layer)
110
143
  return [] unless @layers[layer]
111
144
 
@@ -114,25 +147,96 @@ module GD
114
147
  end
115
148
  end
116
149
 
150
+ # Returns all features in the map.
151
+ #
152
+ # @return [Array<Feature>]
117
153
  def features
118
154
  @layers.values.flatten.map do |item|
119
155
  item.is_a?(Array) ? item.last : item
120
156
  end
121
157
  end
122
158
 
123
- # -----------------------------------
124
- # GeoJSON input (unchanged behavior)
125
- # -----------------------------------
159
+ # Creates a single text label for a named linear feature (LineString or
160
+ # MultiLineString), avoiding duplicate labels for the same named entity.
161
+ #
162
+ # Many datasets (especially OSM) split a single logical entity
163
+ # (rivers, streets, railways, etc.) into multiple line features that
164
+ # all share the same name. This method ensures that:
165
+ #
166
+ # - Only one label is created per unique entity name
167
+ # - The label is placed on a representative segment of the geometry
168
+ # - The logic is independent of the feature's semantic layer (water, road, rail)
169
+ #
170
+ # Labels are rendered using a PointsLayer because libgd-gis does not
171
+ # support text rendering directly on line geometries.
172
+ #
173
+ # The label position is chosen as the midpoint of the line coordinates.
174
+ # This is a simple heuristic that provides a reasonable placement without
175
+ # requiring geometry merging or topological analysis.
176
+ #
177
+ # @param feature [GD::GIS::Feature]
178
+ # A feature with a linear geometry and a "name" property.
179
+ #
180
+ # @return [void]
181
+ # Adds a PointsLayer to @points_layers if a label is created.
182
+ #
183
+ # @note
184
+ # This method must be called during feature loading (add_geojson),
185
+ # before rendering. It intentionally does not depend on map style
186
+ # configuration, which is applied later during rendering.
187
+ def maybe_create_line_label(feature)
188
+ return true if @style.global[:label] == false || @style.global[:label].nil?
189
+
190
+ geom = feature.geometry
191
+ return unless LINE_GEOMS.include?(geom["type"])
192
+
193
+ name = feature.properties["name"]
194
+ return if name.nil? || name.empty?
195
+
196
+ key = feature.properties["wikidata"] || name
197
+ return if @used_labels[key]
198
+
199
+ coords = geom["coordinates"]
200
+ coords = coords.flatten(1) if geom["type"] == "MultiLineString"
201
+ return if coords.size < 2
202
+
203
+ lon, lat = coords[coords.size / 2]
204
+
205
+ @points_layers << GD::GIS::PointsLayer.new(
206
+ [feature],
207
+ lon: ->(_) { lon },
208
+ lat: ->(_) { lat },
209
+ icon: @style.global[:label][:icon],
210
+ label: ->(_) { name },
211
+ font: @style.global[:label][:font] || GD::GIS::FontHelper.random,
212
+ size: @style.global[:label][:size] || (6..20).to_a.sample,
213
+ color: @style.global[:label][:color] || GD::GIS::ColorHelpers.random_rgba
214
+ )
215
+
216
+ @used_labels[key] = true
217
+ end
218
+
219
+ # Loads features from a GeoJSON file.
220
+ #
221
+ # This method:
222
+ # - Normalizes CRS
223
+ # - Classifies features into semantic layers
224
+ # - Creates overlay layers when needed (points)
225
+ #
226
+ # @param path [String] path to GeoJSON file
227
+ # @return [void]
126
228
  def add_geojson(path)
127
229
  features = LayerGeoJSON.load(path)
128
230
 
129
231
  features.each do |feature|
232
+ maybe_create_line_label(feature)
233
+
130
234
  case feature.layer
131
235
  when :water
132
236
  kind =
133
237
  case (feature.properties["objeto"] || feature.properties["waterway"]).to_s.downcase
134
- when /river|río/ then :river
135
- when /stream|arroyo/ then :stream
238
+ when /river|río|canal/ then :river
239
+ when /stream|arroyo/ then :stream
136
240
  else :minor
137
241
  end
138
242
 
@@ -145,26 +249,32 @@ module GD
145
249
  @layers[:park] << feature
146
250
 
147
251
  when :track
148
- # elegí una:
149
- @layers[:minor] << feature
150
- # o @layers[:street] << feature
252
+ @layers[:track] << feature
151
253
  else
152
254
  geom_type = feature.geometry["type"]
153
255
 
154
256
  if geom_type == "Point"
155
- points_style = @style.points or
156
- raise ArgumentError, "Style error: missing 'points' section"
257
+ points_style = @style.points || begin
258
+ warn "Style error: missing 'points' section"
259
+ end
157
260
 
158
- font = points_style[:font] or
159
- raise ArgumentError, "Style error: points.font is required"
261
+ font = points_style[:font] || begin
262
+ warn "[libgd-gis] points.font not defined in style, using random system font"
263
+ GD::GIS::FontHelper.random
264
+ end
160
265
 
161
- size = points_style[:size] or
162
- raise ArgumentError, "Style error: points.size is required"
266
+ size = points_style[:size] || begin
267
+ warn "[libgd-gis] points.font size not defined in style, using random system font size"
268
+ (6..14).to_a.sample
269
+ end
163
270
 
164
271
  raw_color = points_style[:color]
165
272
  color = @style.normalize_color(raw_color)
166
273
 
167
- icon = points_style.key?(:icon_fill) && points_style.key?(:icon_stroke) ? [points_style[:icon_stroke], points_style[:icon_stroke]] : nil
274
+ icon = if points_style.key?(:icon_fill) && points_style.key?(:icon_stroke)
275
+ [points_style[:icon_stroke],
276
+ points_style[:icon_stroke]]
277
+ end
168
278
  icon = points_style.key?(:icon) ? points_style[:icon] : nil if icon.nil?
169
279
 
170
280
  @points_layers << GD::GIS::PointsLayer.new(
@@ -172,36 +282,52 @@ module GD
172
282
  lon: ->(f) { f.geometry["coordinates"][0] },
173
283
  lat: ->(f) { f.geometry["coordinates"][1] },
174
284
  icon: icon,
175
- label: ->(f) { f.properties["name"] }, # 👈 TEXTO
285
+ label: ->(f) { f.properties["name"] },
176
286
  font: font,
177
287
  size: size,
178
288
  color: color
179
289
  )
180
- elsif geom_type == "LineString" || geom_type == "MultiLineString"
290
+ elsif LINE_GEOMS.include?(geom_type)
181
291
  @layers[:minor] << feature
182
292
  end
183
293
  end
184
294
  end
185
295
  end
186
296
 
187
- # -----------------------------------
188
- # Overlay layers
189
- # -----------------------------------
190
- def add_points(data, **opts)
191
- @points_layers << GD::GIS::PointsLayer.new(data, **opts)
297
+ # Adds a generic points overlay layer.
298
+ #
299
+ # @param data [Enumerable]
300
+ # @param opts [Hash]
301
+ # @return [void]
302
+ def add_points(data, **)
303
+ @points_layers << GD::GIS::PointsLayer.new(data, **)
192
304
  end
193
305
 
194
- def add_lines(features, **opts)
195
- @lines_layers << GD::GIS::LinesLayer.new(features, **opts)
306
+ # Adds a generic lines overlay layer.
307
+ #
308
+ # @param features [Array]
309
+ # @param opts [Hash]
310
+ # @return [void]
311
+ def add_lines(features, **)
312
+ @lines_layers << GD::GIS::LinesLayer.new(features, **)
196
313
  end
197
314
 
198
- def add_polygons(polygons, **opts)
199
- @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **opts)
315
+ # Adds a generic polygons overlay layer.
316
+ #
317
+ # @param polygons [Array]
318
+ # @param opts [Hash]
319
+ # @return [void]
320
+ def add_polygons(polygons, **)
321
+ @polygons_layers << GD::GIS::PolygonsLayer.new(polygons, **)
200
322
  end
201
323
 
202
- # -----------------------------------
203
- # Rendering (LEGACY, UNCHANGED)
204
- # -----------------------------------
324
+ # Renders the map.
325
+ #
326
+ # Chooses between tile rendering and viewport rendering
327
+ # depending on whether width and height are set.
328
+ #
329
+ # @return [void]
330
+ # @raise [RuntimeError] if style is not set
205
331
  def render
206
332
  raise "map.style must be set" unless @style
207
333
 
@@ -211,7 +337,7 @@ module GD
211
337
  render_tiles
212
338
  end
213
339
  end
214
-
340
+
215
341
  def render_tiles
216
342
  raise "map.style must be set" unless @style
217
343
 
@@ -248,12 +374,12 @@ module GD
248
374
  [(x - origin_x).round, (y - origin_y).round]
249
375
  end
250
376
 
251
- # 1️⃣ GeoJSON semantic layers
377
+ # 1. GeoJSON semantic layers
252
378
  @style.order.each do |kind|
253
379
  draw_layer(kind, projection)
254
380
  end
255
381
 
256
- # 2️⃣ Generic overlays
382
+ # 2. Generic overlays
257
383
  @polygons_layers.each { |l| l.render!(@image, projection) }
258
384
  @lines_layers.each { |l| l.render!(@image, projection) }
259
385
  @points_layers.each { |l| l.render!(@image, projection) }
@@ -265,22 +391,16 @@ module GD
265
391
  @image = GD::Image.new(@width, @height)
266
392
  @image.antialias = false
267
393
 
268
- # --------------------------------------------------
269
394
  # 1. Compute global pixel bbox
270
- # --------------------------------------------------
271
395
  min_lng, min_lat, max_lng, max_lat = @bbox
272
396
 
273
397
  x1, y1 = GD::GIS::Projection.lonlat_to_global_px(min_lng, max_lat, @zoom)
274
- x2, y2 = GD::GIS::Projection.lonlat_to_global_px(max_lng, min_lat, @zoom)
398
+ GD::GIS::Projection.lonlat_to_global_px(max_lng, min_lat, @zoom)
275
399
 
276
- # --------------------------------------------------
277
400
  # 2. Fetch tiles
278
- # --------------------------------------------------
279
401
  tiles, = @basemap.fetch_tiles
280
402
 
281
- # --------------------------------------------------
282
403
  # 3. Draw tiles clipped to viewport
283
- # --------------------------------------------------
284
404
  tiles.each do |x, y, file|
285
405
  tile = GD::Image.open(file)
286
406
 
@@ -309,16 +429,12 @@ module GD
309
429
  )
310
430
  end
311
431
 
312
- # --------------------------------------------------
313
432
  # 4. Projection (viewport version)
314
- # --------------------------------------------------
315
433
  projection = lambda do |lon, lat|
316
434
  GD::GIS::Geometry.project(lon, lat, @bbox, @zoom)
317
435
  end
318
436
 
319
- # --------------------------------------------------
320
437
  # 5. REUSE the same render pipeline
321
- # --------------------------------------------------
322
438
  @style.order.each do |kind|
323
439
  draw_layer(kind, projection)
324
440
  end
@@ -328,10 +444,19 @@ module GD
328
444
  @points_layers.each { |l| l.render!(@image, projection) }
329
445
  end
330
446
 
447
+ # Saves the rendered image to disk.
448
+ #
449
+ # @param path [String]
450
+ # @return [void]
331
451
  def save(path)
332
452
  @image.save(path)
333
453
  end
334
454
 
455
+ # Draws a semantic feature layer.
456
+ #
457
+ # @param kind [Symbol]
458
+ # @param projection [#call]
459
+ # @return [void]
335
460
  def draw_layer(kind, projection)
336
461
  items = @layers[kind]
337
462
  return if items.nil? || items.empty?
@@ -340,6 +465,8 @@ module GD
340
465
  case kind
341
466
  when :street, :primary, :motorway, :secondary, :minor
342
467
  @style.roads[kind]
468
+ when :track
469
+ @style.track[kind]
343
470
  when :rail
344
471
  @style.rails
345
472
  when :water
@@ -374,24 +501,22 @@ module GD
374
501
  f = item
375
502
  geom = f.geometry["type"]
376
503
 
377
- if geom == "Polygon" || geom == "MultiPolygon"
504
+ if POLY_GEOMS.include?(geom)
378
505
  f.draw(@image, projection, nil, nil, style)
379
- else
380
- if style[:stroke]
381
- color = GD::Color.rgb(*style[:stroke])
506
+ elsif style[:stroke]
507
+ r, g, b, a = style[:stroke]
508
+ a = 0 if a.nil?
509
+ color = GD::Color.rgba(r, g, b, a)
382
510
 
383
- color = GD::GIS::ColorHelpers.random_vivid if @debug
511
+ color = GD::GIS::ColorHelpers.random_vivid if @debug
384
512
 
385
- width = style[:stroke_width] ? style[:stroke_width].round : 1
386
- width = 1 if width < 1
387
- f.draw(@image, projection, color, width)
388
- end
513
+ width = style[:stroke_width] ? style[:stroke_width].round : 1
514
+ width = 1 if width < 1
515
+ f.draw(@image, projection, color, width)
389
516
  end
390
517
  end
391
518
  end
392
519
  end
393
-
394
520
  end
395
521
  end
396
522
  end
397
-
@@ -1,17 +1,65 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module GD
2
4
  module GIS
5
+ # Coordinate Reference System (CRS) helpers and normalizers.
6
+ #
7
+ # This module defines commonly used CRS identifiers and
8
+ # utilities for normalizing coordinates into a single,
9
+ # consistent representation.
10
+ #
3
11
  module CRS
12
+ # OGC CRS84 (longitude, latitude)
4
13
  CRS84 = "urn:ogc:def:crs:OGC:1.3:CRS84"
14
+
15
+ # EPSG:4326 (latitude, longitude axis order)
5
16
  EPSG4326 = "EPSG:4326"
17
+
18
+ # EPSG:3857 (Web Mercator)
6
19
  EPSG3857 = "EPSG:3857"
7
- GK_ARGENTINA = "EPSG:22195" # Gauss–Krüger Argentina (zone 5 example)
8
20
 
9
- # Normalize any CRS → CRS84 (lon,lat in degrees)
21
+ # Gauss–Krüger Argentina, zone 5
22
+ #
23
+ # Note: This constant represents a *specific* GK zone
24
+ # and is not a generic Gauss–Krüger definition.
25
+ GK_ARGENTINA = "EPSG:22195"
26
+
27
+ # Normalizes coordinates from supported CRS definitions
28
+ # into CRS84 (longitude, latitude in degrees).
29
+ #
30
+ # Supported input CRS:
31
+ # - CRS84
32
+ # - EPSG:4326 (axis order normalization)
33
+ # - EPSG:3857 (Web Mercator)
34
+ # - EPSG:22195 (Gauss–Krüger Argentina, zone 5)
35
+ #
36
+ # All outputs are returned as:
37
+ # [longitude, latitude] in degrees
38
+ #
39
+ # ⚠️ Projection conversions are intended for mapping
40
+ # and visualization, not for high-precision geodesy.
41
+ #
10
42
  class Normalizer
43
+ # Creates a new CRS normalizer.
44
+ #
45
+ # @param crs [String, Symbol, nil]
46
+ # CRS identifier; defaults to CRS84 if nil
11
47
  def initialize(crs)
12
48
  @crs = normalize_name(crs)
13
49
  end
14
50
 
51
+ # Normalizes a coordinate pair into CRS84.
52
+ #
53
+ # @param lon [Numeric]
54
+ # first coordinate (meaning depends on input CRS)
55
+ # @param lat [Numeric]
56
+ # second coordinate (meaning depends on input CRS)
57
+ #
58
+ # @return [Array<Float>]
59
+ # normalized [longitude, latitude] in degrees
60
+ #
61
+ # @raise [RuntimeError]
62
+ # if the CRS is not supported
15
63
  def normalize(lon, lat)
16
64
  case @crs
17
65
  when CRS84
@@ -34,52 +82,67 @@ module GD
34
82
 
35
83
  private
36
84
 
85
+ # Normalizes a CRS name into a comparable string.
86
+ #
87
+ # @param name [Object]
88
+ # @return [String]
37
89
  def normalize_name(name)
38
90
  return CRS84 if name.nil?
91
+
39
92
  name.to_s.strip
40
93
  end
41
94
 
42
- # Web Mercator WGS84
95
+ # Converts Web Mercator coordinates to WGS84.
96
+ #
97
+ # @param x [Numeric] X coordinate in meters
98
+ # @param y [Numeric] Y coordinate in meters
99
+ # @return [Array<Float>] [longitude, latitude] in degrees
43
100
  def mercator_to_wgs84(x, y)
44
101
  r = 6378137.0
45
102
  lon = (x / r) * 180.0 / Math::PI
46
- lat = (2 * Math.atan(Math.exp(y / r)) - Math::PI / 2) * 180.0 / Math::PI
103
+ lat = ((2 * Math.atan(Math.exp(y / r))) - (Math::PI / 2)) * 180.0 / Math::PI
47
104
  [lon, lat]
48
105
  end
49
106
 
50
- # Gauss–Krüger Argentina (Zone 5) WGS84
51
- # This is enough precision for mapping
107
+ # Converts Gauss–Krüger Argentina (zone 5) coordinates to WGS84.
108
+ #
109
+ # This implementation provides sufficient accuracy for
110
+ # cartographic rendering and visualization.
111
+ #
112
+ # @param easting [Numeric] easting (meters)
113
+ # @param northing [Numeric] northing (meters)
114
+ # @return [Array<Float>] [longitude, latitude] in degrees
52
115
  def gk_to_wgs84(easting, northing)
53
116
  # Parameters for Argentina GK Zone 5
54
117
  a = 6378137.0
55
118
  f = 1 / 298.257223563
56
- e2 = 2*f - f*f
57
- lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
119
+ e2 = (2 * f) - (f * f)
120
+ lon0 = -60.0 * Math::PI / 180.0 # central meridian zone 5
58
121
 
59
122
  x = easting - 500000.0
60
123
  y = northing
61
124
 
62
125
  m = y
63
- mu = m / (a * (1 - e2/4 - 3*e2*e2/64))
126
+ mu = m / (a * (1 - (e2 / 4) - (3 * e2 * e2 / 64)))
64
127
 
65
128
  e1 = (1 - Math.sqrt(1 - e2)) / (1 + Math.sqrt(1 - e2))
66
129
 
67
- j1 = 3*e1/2 - 27*e1**3/32
68
- j2 = 21*e1**2/16 - 55*e1**4/32
130
+ j1 = (3 * e1 / 2) - (27 * (e1**3) / 32)
131
+ j2 = (21 * (e1**2) / 16) - (55 * (e1**4) / 32)
69
132
 
70
- fp = mu + j1*Math.sin(2*mu) + j2*Math.sin(4*mu)
133
+ fp = mu + (j1 * Math.sin(2 * mu)) + (j2 * Math.sin(4 * mu))
71
134
 
72
- c1 = e2 * Math.cos(fp)**2
135
+ c1 = e2 * (Math.cos(fp)**2)
73
136
  t1 = Math.tan(fp)**2
74
- r1 = a * (1 - e2) / (1 - e2 * Math.sin(fp)**2)**1.5
75
- n1 = a / Math.sqrt(1 - e2 * Math.sin(fp)**2)
137
+ r1 = a * (1 - e2) / ((1 - (e2 * (Math.sin(fp)**2)))**1.5)
138
+ n1 = a / Math.sqrt(1 - (e2 * (Math.sin(fp)**2)))
76
139
 
77
140
  d = x / n1
78
141
 
79
- lat = fp - (n1*Math.tan(fp)/r1) *
80
- (d**2/2 - (5 + 3*t1 + 10*c1)*d**4/24)
142
+ lat = fp - ((n1 * Math.tan(fp) / r1) *
143
+ (((d**2) / 2) - ((5 + (3 * t1) + (10 * c1)) * (d**4) / 24)))
81
144
 
82
- lon = lon0 + (d - (1 + 2*t1 + c1)*d**3/6) / Math.cos(fp)
145
+ lon = lon0 + ((d - ((1 + (2 * t1) + c1) * (d**3) / 6)) / Math.cos(fp))
83
146
 
84
147
  [lon * 180.0 / Math::PI, lat * 180.0 / Math::PI]
85
148
  end