Class: TimezoneFinder::Helpers

Inherits:
Object
  • Object
show all
Defined in:
lib/timezone_finder/helpers.rb

Class Method Summary collapse

Class Method Details

.all_the_same(pointer, length, id_list) ⇒ Object



167
168
169
170
171
172
173
174
175
176
177
178
179
180
# File 'lib/timezone_finder/helpers.rb', line 167

def self.all_the_same(pointer, length, id_list)
  # List mustn't be empty or Null
  # There is at least one

  element = id_list[pointer]
  pointer += 1

  while pointer < length
    return -1 if element != id_list[pointer]
    pointer += 1
  end

  element
end

.cartesian2coords(x, y, z) ⇒ Object



194
195
196
# File 'lib/timezone_finder/helpers.rb', line 194

def self.cartesian2coords(x, y, z)
  [degrees(Math.atan2(y, x)), degrees(Math.asin(z))]
end

.cartesian2rad(x, y, z) ⇒ Object



182
183
184
# File 'lib/timezone_finder/helpers.rb', line 182

def self.cartesian2rad(x, y, z)
  [Math.atan2(y, x), Math.asin(z)]
end

.compute_min_distance(lng_rad, lat_rad, p0_lng, p0_lat, pm1_lng, pm1_lat, p1_lng, p1_lat) ⇒ Object

:param lng_rad: lng of px in radians :param lat_rad: lat of px in radians :param p0_lng: lng of p0 in radians :param p0_lat: lat of p0 in radians :param pm1_lng: lng of pm1 in radians :param pm1_lat: lat of pm1 in radians :param p1_lng: lng of p1 in radians :param p1_lat: lat of p1 in radians :return: shortest distance between pX and the polygon section (pm1—p0—p1) in radians



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
# File 'lib/timezone_finder/helpers.rb', line 249

def self.compute_min_distance(lng_rad, lat_rad, p0_lng, p0_lat, pm1_lng, pm1_lat, p1_lng, p1_lat)
  # rotate coordinate system (= all the points) so that p0 would have lat_rad=lng_rad=0 (=origin)
  # z rotation is simply substracting the lng_rad
  # convert the points to the cartesian coorinate system
  px_cartesian = coords2cartesian(lng_rad - p0_lng, lat_rad)
  p1_cartesian = coords2cartesian(p1_lng - p0_lng, p1_lat)
  pm1_cartesian = coords2cartesian(pm1_lng - p0_lng, pm1_lat)

  px_cartesian = y_rotate(p0_lat, px_cartesian)
  p1_cartesian = y_rotate(p0_lat, p1_cartesian)
  pm1_cartesian = y_rotate(p0_lat, pm1_cartesian)

  # for both p1 and pm1 separately do:

  # rotate coordinate system so that this point also has lat_p1_rad=0 and lng_p1_rad>0 (p0 does not change!)
  rotation_rad = Math.atan2(p1_cartesian[2], p1_cartesian[1])
  p1_cartesian = x_rotate(rotation_rad, p1_cartesian)
  lng_p1_rad = Math.atan2(p1_cartesian[1], p1_cartesian[0])
  px_retrans_rad = cartesian2rad(*x_rotate(rotation_rad, px_cartesian))

  # if lng_rad of px is between 0 (<-point1) and lng_rad of point 2:
  # the distance between point x and the 'equator' is the shortest
  # if the point is not between p0 and p1 the distance to the closest of the two points should be used
  # so clamp/clip the lng_rad of px to the interval of [0; lng_rad p1] and compute the distance with it
  temp_distance = distance_to_point_on_equator(px_retrans_rad[0], px_retrans_rad[1],
                                               [[px_retrans_rad[0], lng_p1_rad].min, 0].max)

  # ATTENTION: vars are being reused. p1 is actually pm1 here!
  rotation_rad = Math.atan2(pm1_cartesian[2], pm1_cartesian[1])
  p1_cartesian = x_rotate(rotation_rad, pm1_cartesian)
  lng_p1_rad = Math.atan2(p1_cartesian[1], p1_cartesian[0])
  px_retrans_rad = cartesian2rad(*x_rotate(rotation_rad, px_cartesian))

  [
    temp_distance,
    distance_to_point_on_equator(px_retrans_rad[0], px_retrans_rad[1],
                                 [[px_retrans_rad[0], lng_p1_rad].min, 0].max)
  ].min
end

.coord2int(double) ⇒ Object



293
294
295
# File 'lib/timezone_finder/helpers.rb', line 293

def self.coord2int(double)
  (double * 10**7).to_i
end

.coords2cartesian(lng_rad, lat_rad) ⇒ Object



214
215
216
# File 'lib/timezone_finder/helpers.rb', line 214

def self.coords2cartesian(lng_rad, lat_rad)
  [Math.cos(lng_rad) * Math.cos(lat_rad), Math.sin(lng_rad) * Math.cos(lat_rad), Math.sin(lat_rad)]
end

.degrees(x) ⇒ Object



190
191
192
# File 'lib/timezone_finder/helpers.rb', line 190

def self.degrees(x)
  x * 180.0 / Math::PI
end

.distance_to_point_on_equator(lng_rad, lat_rad, lng_rad_p1) ⇒ Object

uses the simplified haversine formula for this special case (lat_p1 = 0) :param lng_rad: the longitude of the point in radians :param lat_rad: the latitude of the point :param lng_rad_p1: the latitude of the point1 on the equator (lat=0) :return: distance between the point and p1 (lng_rad_p1,0) in km this is only an approximation since the earth is not a real sphere



224
225
226
227
# File 'lib/timezone_finder/helpers.rb', line 224

def self.distance_to_point_on_equator(lng_rad, lat_rad, lng_rad_p1)
  # 2* for the distance in rad and * 12742 (mean diameter of earth) for the distance in km
  12_742 * Math.asin(Math.sqrt(Math.sin(lat_rad / 2.0)**2 + Math.cos(lat_rad) * Math.sin((lng_rad - lng_rad_p1) / 2.0)**2))
end

.distance_to_polygon(lng_rad, lat_rad, nr_points, points) ⇒ Object



332
333
334
335
336
337
338
339
340
341
# File 'lib/timezone_finder/helpers.rb', line 332

def self.distance_to_polygon(lng_rad, lat_rad, nr_points, points)
  min_distance = 40_100_000

  (0...nr_points).each do |i|
    min_distance = [min_distance, haversine(lng_rad, lat_rad, radians(int2coord(points[0][i])),
                                            radians(int2coord(points[1][i])))].min
  end

  min_distance
end

.distance_to_polygon_exact(lng_rad, lat_rad, nr_points, points, trans_points) ⇒ Object



297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
# File 'lib/timezone_finder/helpers.rb', line 297

def self.distance_to_polygon_exact(lng_rad, lat_rad, nr_points, points, trans_points)
  # transform all points (long long) to coords
  (0...nr_points).each do |i|
    trans_points[0][i] = radians(int2coord(points[0][i]))
    trans_points[1][i] = radians(int2coord(points[1][i]))
  end

  # check points -2, -1, 0 first
  pm1_lng = trans_points[0][0]
  pm1_lat = trans_points[1][0]

  p1_lng = trans_points[0][-2]
  p1_lat = trans_points[1][-2]
  min_distance = compute_min_distance(lng_rad, lat_rad, trans_points[0][-1], trans_points[1][-1], pm1_lng, pm1_lat,
                                      p1_lng, p1_lat)

  index_p0 = 1
  index_p1 = 2
  (0...(((nr_points / 2.0) - 1).ceil.to_i)).each do |_i|
    p1_lng = trans_points[0][index_p1]
    p1_lat = trans_points[1][index_p1]

    min_distance = [min_distance,
                    compute_min_distance(lng_rad, lat_rad, trans_points[0][index_p0], trans_points[1][index_p0],
                                         pm1_lng, pm1_lat, p1_lng, p1_lat)].min

    index_p0 += 2
    index_p1 += 2
    pm1_lng = p1_lng
    pm1_lat = p1_lat
  end

  min_distance
end

.fromfile(file, unsigned, byte_width, count) ⇒ Object

Ruby original like numpy.fromfile



345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/timezone_finder/helpers.rb', line 345

def self.fromfile(file, unsigned, byte_width, count)
  if unsigned
    case byte_width
    when 2
      unpack_format = 'S<*'
    end
  else
    case byte_width
    when 4
      unpack_format = 'l<*'
    when 8
      unpack_format = 'q<*'
    end
  end

  unless unpack_format
    raise "#{unsigned ? 'unsigned' : 'signed'} #{byte_width}-byte width is not supported in fromfile"
  end

  file.read(count * byte_width).unpack(unpack_format)
end

.haversine(lng_p1, lat_p1, lng_p2, lat_p2) ⇒ Object

:param lng_p1: the longitude of point 1 in radians :param lat_p1: the latitude of point 1 in radians :param lng_p2: the longitude of point 1 in radians :param lat_p2: the latitude of point 1 in radians :return: distance between p1 and p2 in km this is only an approximation since the earth is not a real sphere



235
236
237
238
# File 'lib/timezone_finder/helpers.rb', line 235

def self.haversine(lng_p1, lat_p1, lng_p2, lat_p2)
  # 2* for the distance in rad and * 12742(mean diameter of earth) for the distance in km
  12_742 * Math.asin(Math.sqrt(Math.sin((lat_p1 - lat_p2) / 2.0)**2 + Math.cos(lat_p2) * Math.cos(lat_p1) * Math.sin((lng_p1 - lng_p2) / 2.0)**2))
end

.inside_polygon(x, y, coords) ⇒ Object



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/timezone_finder/helpers.rb', line 109

def self.inside_polygon(x, y, coords)
  wn = 0
  i = 0
  y1 = coords[1][0]
  # TODO: why start with both y1=y2= y[0]?
  coords[1].each do |y2|
    if y1 < y
      if y2 >= y
        x1 = coords[0][i - 1]
        x2 = coords[0][i]
        # print(long2coord(x), long2coord(y), long2coord(x1), long2coord(x2), long2coord(y1), long2coord(y2),
        #       position_to_line(x, y, x1, x2, y1, y2))
        if position_to_line(x, y, x1, x2, y1, y2) == -1
          # point is left of line
          # return true when its on the line?! this is very unlikely to happen!
          # and would need to be checked every time!
          wn += 1
        end
      end
    else
      if y2 < y
        x1 = coords[0][i - 1]
        x2 = coords[0][i]
        if position_to_line(x, y, x1, x2, y1, y2) == 1
          # point is right of line
          wn -= 1
        end
      end
    end

    y1 = y2
    i += 1
  end

  y1 = coords[1][-1]
  y2 = coords[1][0]
  if y1 < y
    if y2 >= y
      x1 = coords[0][-1]
      x2 = coords[0][0]
      if position_to_line(x, y, x1, x2, y1, y2) == -1
        # point is left of line
        wn += 1
      end
    end
  else
    if y2 < y
      x1 = coords[0][-1]
      x2 = coords[0][0]
      if position_to_line(x, y, x1, x2, y1, y2) == 1
        # point is right of line
        wn -= 1
      end
    end
  end
  wn != 0
end

.int2coord(int32) ⇒ Object



289
290
291
# File 'lib/timezone_finder/helpers.rb', line 289

def self.int2coord(int32)
  int32.fdiv(10**7)
end

.position_to_line(x, y, x1, x2, y1, y2) ⇒ Object

tests if a point pX(x,y) is Left|On|Right of an infinite line from p1 to p2

Return: -1 for pX left of the line from! p1 to! p2
        0 for pX on the line [is not needed]
        1 for pX  right of the line
        this approach is only valid because we already know that y lies within ]y1;y2]


10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/timezone_finder/helpers.rb', line 10

def self.position_to_line(x, y, x1, x2, y1, y2)
  if x1 < x2
    # p2 is further right than p1
    if x > x2
      # pX is further right than p2,
      if y1 > y2
        return -1
      else
        return 1
      end
    end

    if x < x1
      # pX is further left than p1
      if y1 > y2
        # so it has to be right of the line p1-p2
        return 1
      else
        return -1
      end
    end

    x1gtx2 = false

  else
    # p1 is further right than p2
    if x > x1
      # pX is further right than p1,
      if y1 > y2
        # so it has to be left of the line p1-p2
        return -1
      else
        return 1
      end
    end

    if x < x2
      # pX is further left than p2,
      if y1 > y2
        # so it has to be right of the line p1-p2
        return 1
      else
        return -1
      end
    end

    # TODO: is not return also accepted
    if x1 == x2 && x == x1
      # could also be equal
      return 0
    end

    # x1 greater than x2
    x1gtx2 = true
  end

  # x is between [x1;x2]
  # compute the x-intersection of the point with the line p1-p2
  # delta_y cannot be 0 here because of the condition 'y lies within ]y1;y2]'
  # NOTE: bracket placement is important here (we are dealing with 64-bit ints!). first divide then multiply!
  delta_x = ((y - y1) * (x2 - x1).fdiv(y2 - y1)) + x1 - x

  if delta_x > 0
    if x1gtx2
      if y1 > y2
        return 1
      else
        return -1
      end

    else
      if y1 > y2
        return 1
      else
        return -1
      end
    end

  elsif delta_x == 0
    return 0

  else
    if x1gtx2
      if y1 > y2
        return -1
      else
        return 1
      end

    else
      if y1 > y2
        return -1
      else
        return 1
      end
    end
  end
end

.radians(x) ⇒ Object



186
187
188
# File 'lib/timezone_finder/helpers.rb', line 186

def self.radians(x)
  x * Math::PI / 180.0
end

.x_rotate(rad, point) ⇒ Object



198
199
200
201
202
203
204
# File 'lib/timezone_finder/helpers.rb', line 198

def self.x_rotate(rad, point)
  # Attention: this rotation uses radians!
  # x stays the same
  sin_rad = Math.sin(rad)
  cos_rad = Math.cos(rad)
  [point[0], point[1] * cos_rad + point[2] * sin_rad, point[2] * cos_rad - point[1] * sin_rad]
end

.y_rotate(rad, point) ⇒ Object



206
207
208
209
210
211
212
# File 'lib/timezone_finder/helpers.rb', line 206

def self.y_rotate(rad, point)
  # y stays the same
  # this is actually a rotation with -rad (use symmetry of sin/cos)
  sin_rad = Math.sin(rad)
  cos_rad = Math.cos(rad)
  [point[0] * cos_rad + point[2] * sin_rad, point[1], point[2] * cos_rad - point[0] * sin_rad]
end