mapplz 0.1.3 → 0.1.4

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.
Files changed (4) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +23 -9
  3. data/lib/mapplz.rb +566 -302
  4. metadata +2 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: e5045761f3327fe2be26fb1769108ca424ab1689
4
- data.tar.gz: 6cf61d9ec132edbd8911b73e0317681c3c68ec43
3
+ metadata.gz: 4da45e726fdc80c0a4ce530e6522f31ed8c8035f
4
+ data.tar.gz: 5595cad9a95f0ec4b638c183d6f8f6c268ff4bab
5
5
  SHA512:
6
- metadata.gz: 872d06ba820f726ade5184ee11b6d1977009e8209a7b0792b900eb43180abceeffcf77f3096c4b24c1b29bdd8991614af4bcbc725bfe7d9f68e0961fddea30cd
7
- data.tar.gz: ae3c23126048c4a306701e3cf64f22ce7184bb45ab3143fed845826194555bb289273110e51ebd0e7197e3074da82a53ed3a25b0c35cb55f26629f529d83320e
6
+ metadata.gz: 6d204b6cc48b3803adb797ff19dbf9c15b4187b2db01dc13852264761abd7e4148f478e423414ef7f81ce8081aebc4a79b48d98aa6187c315f19d119f19c1669
7
+ data.tar.gz: 6e050eea53d3d0f6fd844eb0844267618fbde3a5130c9424a967126bcc99345e024fde465f34437b66cecfd6027b89a07495d6b9d5b3aa2314c08d77000d5685
data/README.md CHANGED
@@ -110,6 +110,13 @@ mapplz.where('layer = ?', name_of_layer)
110
110
  # get a count
111
111
  mapplz.count
112
112
  mapplz.count('layer = ?', name_of_layer)
113
+
114
+ # near a point
115
+ mapplz.near([lat, lng])
116
+ mapplz.near([lat, lng], max: 10)
117
+
118
+ # in an area
119
+ mapplz.inside([point1, point2, point3, point1])
113
120
  ```
114
121
 
115
122
  Queries are returned as an array of GeoItems, which each can be exported as GeoJSON or WKT
@@ -119,6 +126,21 @@ my_features = @mapper.where('points > 10')
119
126
  collection = { type: 'FeatureCollection', features: my_features.map { |feature| JSON.parse(feature.to_geojson) } }
120
127
  ```
121
128
 
129
+ ## Files
130
+
131
+ MapPLZ can be passed a CSV or GeoJSON file.
132
+
133
+ ```
134
+ @mapstore < File.open('test.csv')
135
+ @mapstore < File.open('test.geojson')
136
+ ```
137
+
138
+ If you have gdal installed, you can import files in most formats parseable by the ```ogr2ogr``` command line tool.
139
+
140
+ ```
141
+ @mapstore < File.open('test.shp')
142
+ ```
143
+
122
144
  ## Databases
123
145
 
124
146
  You can store geodata in SQLite/Spatialite, Postgres/PostGIS, or MongoDB.
@@ -147,6 +169,7 @@ require 'mongo'
147
169
  mongo_client = Mongo::MongoClient.new
148
170
  database = mongo_client['mapplz']
149
171
  collection = database['geoitems']
172
+ collection.create_index(geo: Mongo::GEO2DSPHERE)
150
173
  mapstore = MapPLZ.new(collection)
151
174
  mapstore.choose_db('mongodb')
152
175
 
@@ -167,15 +190,6 @@ mapstore = MapPLZ.new(db)
167
190
  mapstore.choose_db('spatialite')
168
191
  ```
169
192
 
170
- ### COMING SOON
171
-
172
- ```
173
- # near a point
174
- mapplz.near([lat, lng])
175
-
176
- # in an area
177
- mapplz.inside([point1, point2, point3, point1])
178
- ```
179
193
 
180
194
  ## Language
181
195
  You can make a map super quickly by using the MapPLZ language. A MapPLZ map
data/lib/mapplz.rb CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  require 'sql_parser'
4
4
  require 'json'
5
+ require 'csv'
6
+ require 'geokdtree'
7
+ require 'digest'
5
8
  include Leaflet::ViewHelpers
6
9
 
7
10
  # MapPLZ datastore
@@ -33,20 +36,33 @@ class MapPLZ
33
36
  end
34
37
 
35
38
  def add(user_geo, lonlat = false)
36
- geo_objects = standardize_geo(user_geo, lonlat)
39
+ begin
40
+ geo_objects = MapPLZ.standardize_geo(user_geo, lonlat, @db)
41
+ rescue MapPLZException
42
+ geo_objects = code(user_geo)
43
+ end
37
44
 
38
45
  if @db_type == 'array'
39
46
  @my_array += geo_objects
40
47
  elsif @db_type == 'mongodb'
41
48
  geo_objects.each do |geo_object|
42
- reply = @db_client.insert(geo_object)
49
+ save_obj = geo_object.clone
50
+ save_obj.delete(:_id)
51
+ save_obj.delete(:lat)
52
+ save_obj.delete(:lng)
53
+ save_obj.delete(:path)
54
+ save_obj.delete(:centroid)
55
+ save_obj.delete(:type)
56
+ save_obj[:geo] = JSON.parse(geo_object.to_geojson)['geometry']
57
+ reply = @db_client.insert(save_obj)
43
58
  geo_object[:_id] = reply.to_s
44
59
  end
45
60
  elsif @db_type == 'postgis' || @db_type == 'spatialite'
46
61
  geo_objects.each do |geo_object|
47
62
  geom = geo_object.to_wkt
48
63
  if @db_type == 'postgis'
49
- reply = @db_client.exec("INSERT INTO mapplz (label, geom) VALUES ('#{geo_object[:label] || ''}', ST_GeomFromText('#{geom}')) RETURNING id")
64
+ geojson_props = (JSON.parse(geo_object.to_geojson)['properties'] || {})
65
+ reply = @db_client.exec("INSERT INTO mapplz (properties, geom) VALUES ('#{geojson_props.to_json}', ST_GeomFromText('#{geom}')) RETURNING id")
50
66
  elsif @db_type == 'spatialite'
51
67
  reply = @db_client.execute("INSERT INTO mapplz (label, geom) VALUES ('#{geo_object[:label] || ''}', AsText('#{geom}')) RETURNING id")
52
68
  end
@@ -63,20 +79,12 @@ class MapPLZ
63
79
 
64
80
  def count(where_clause = nil, add_on = nil)
65
81
  results = query(where_clause, add_on)
66
- if @db_type == 'array'
67
- results.length
68
- elsif @db_type == 'mongodb'
69
- if where_clause.present?
70
- # @db_client.find().count
71
- else
72
- @db_client.count
73
- end
74
- else
75
- results.count
76
- end
82
+ results.length
77
83
  end
78
84
 
79
85
  def query(where_clause = nil, add_on = nil)
86
+ geo_results = []
87
+
80
88
  if where_clause.present?
81
89
  if @db_type == 'array'
82
90
  geo_results = query_array(where_clause, add_on)
@@ -96,17 +104,19 @@ class MapPLZ
96
104
  end
97
105
 
98
106
  cursor = @db_client.find(mongo_conditions)
99
- elsif @db_type == 'postgis' || @db_type == 'spatialite'
107
+ elsif @db_type == 'postgis'
108
+ where_prop = where_clause.strip.split(' ')[0]
109
+ where_clause = where_clause.gsub(where_prop, "json_extract_path_text(properties, '#{where_prop}')")
110
+
111
+ cursor = @db_client.exec("SELECT id, ST_AsText(geom) AS geo, properties FROM mapplz WHERE #{where_clause}")
112
+ elsif @db_type == 'spatialite'
100
113
  if add_on.is_a?(String)
101
114
  where_clause = where_clause.gsub('?', "'#{add_on}'")
102
115
  elsif add_on.is_a?(Integer) || add_on.is_a?(Float)
103
116
  where_clause = where_clause.gsub('?', "#{add_on}")
104
117
  end
105
118
 
106
- cursor = @db_client.exec("SELECT id, ST_AsText(geom) AS geom, label FROM mapplz WHERE #{where_clause}") if @db_type == 'postgis'
107
- cursor = @db_client.execute("SELECT id, AsText(geom) AS geom, label FROM mapplz WHERE #{where_clause}") if @db_type == 'spatialite'
108
- else
109
- # @my_db.where(where_clause, add_on)
119
+ cursor = @db_client.execute("SELECT id, AsText(geom) AS geo, label FROM mapplz WHERE #{where_clause}")
110
120
  end
111
121
  else
112
122
  # query all
@@ -115,47 +125,15 @@ class MapPLZ
115
125
  elsif @db_type == 'mongodb'
116
126
  cursor = @db_client.find
117
127
  elsif @db_type == 'postgis'
118
- cursor = @db_client.exec('SELECT id, ST_AsText(geom) AS geom, label FROM mapplz')
128
+ cursor = @db_client.exec('SELECT id, ST_AsText(geom) AS geo, properties FROM mapplz')
119
129
  elsif @db_type == 'spatialite'
120
- cursor = @db_client.execute('SELECT id, AsText(geom) AS geom, label FROM mapplz')
130
+ cursor = @db_client.execute('SELECT id, AsText(geom) AS geo, label FROM mapplz')
121
131
  else
122
132
  # @my_db.all
123
133
  end
124
134
  end
125
135
 
126
- unless cursor.nil?
127
- geo_results = []
128
- cursor.each do |geo_result|
129
- geo_item = GeoItem.new
130
- geo_result.keys.each do |key|
131
- next if [:geom].include?(key.to_sym)
132
- geo_item[key.to_sym] = geo_result[key]
133
- end
134
-
135
- if @db_type == 'postgis' || @db_type == 'spatialite'
136
- geom = (geo_result['geom'] || geo_result[:geom]).upcase
137
- if geom.index('POINT')
138
- coordinates = geom.gsub('POINT', '').gsub('(', '').gsub(')', '').split(' ')
139
- geo_item[:lat] = coordinates[1].to_f
140
- geo_item[:lng] = coordinates[0].to_f
141
- elsif geom.index('LINESTRING')
142
- line_nodes = geom.gsub('LINESTRING', '').gsub('(', '').gsub(')', '').split(',')
143
- geo_item[:path] = line_nodes.map do |pt|
144
- pt = pt.split(' ')
145
- [pt[1].to_f, pt[0].to_f]
146
- end
147
- elsif geom.index('POLYGON')
148
- line_nodes = geom.gsub('POLYGON', '').gsub('(', '').gsub(')', '').split(', ')
149
- geo_item[:path] = line_nodes.map do |pt|
150
- pt = pt.split(' ')
151
- [pt[1].to_f, pt[0].to_f]
152
- end
153
- end
154
- end
155
- geo_results << geo_item
156
- end
157
- end
158
-
136
+ geo_results += read_cursor(cursor)
159
137
  geo_results
160
138
  end
161
139
 
@@ -255,12 +233,415 @@ class MapPLZ
255
233
  render_text + '</script>'
256
234
  end
257
235
 
236
+ def near(user_geo, limit = 10, max = 40_010_000, lon_lat = false)
237
+ max = max.to_f # send max in meters
238
+ limit = limit.to_i
239
+
240
+ if user_geo.is_a?(Hash)
241
+ lat = user_geo[:lat] || user_geo['lat'] || user_geo[:latitude] || user_geo['latitude']
242
+ lng = user_geo[:lng] || user_geo['lng'] || user_geo[:longitude] || user_geo['longitude']
243
+ user_geo = [lat.to_f, lng.to_f]
244
+ elsif user_geo.is_a?(Array)
245
+ user_geo.reverse! if lon_lat
246
+ else
247
+ fail 'must query near a point'
248
+ end
249
+
250
+ lat = user_geo[0].to_f
251
+ lng = user_geo[1].to_f
252
+ wkt = "POINT(#{lng} #{lat})"
253
+ geo_results = []
254
+
255
+ if @db_type == 'array'
256
+ @my_array.sort! do |a, b|
257
+ a_distance = a.distance_from([lat, lng])
258
+ b_distance = b.distance_from([lat, lng])
259
+ a_distance <=> b_distance
260
+ end
261
+ geo_results = @my_array.slice(0, limit)
262
+ elsif @db_type == 'mongodb'
263
+ cursor = @db_client.find(geo: { '$nearSphere' => { '$geometry' => { type: 'Point', coordinates: [lng, lat] }, '$maxDistance' => max } })
264
+ elsif @db_type == 'postgis'
265
+ cursor = @db_client.exec("SELECT id, ST_AsText(geom) AS geo, properties, ST_Distance(start.geom::geography, ST_GeomFromText('#{wkt}')::geography) AS distance FROM mapplz AS start WHERE ST_Distance(start.geom::geography, ST_GeomFromText('#{wkt}')::geography) <= #{max} ORDER BY distance LIMIT #{limit}")
266
+ elsif @db_type == 'spatialite'
267
+ cursor = @db_client.execute("SELECT id, AsText(geom) AS geo, label, Distance(start.geom, AsText('#{wkt}')) AS distance FROM mapplz AS start WHERE Distance(start.geom, AsText('#{wkt}')) <= #{max} ORDER BY distance LIMIT #{limit}")
268
+ end
269
+
270
+ geo_results += read_cursor(cursor)
271
+ geo_results
272
+ end
273
+
274
+ def inside(user_geo)
275
+ geo_results = []
276
+
277
+ # accept [point1, point2, point3, point1] as a polygon search area
278
+ if user_geo.is_a?(Array) && user_geo[0].is_a?(Array) && !user_geo[0][0].is_a?(Array)
279
+ user_geo = [user_geo]
280
+ end
281
+
282
+ search_areas = MapPLZ.standardize_geo(user_geo)
283
+
284
+ search_areas.each do |search_area|
285
+ next unless search_area.key?(:path)
286
+ wkt = search_area.to_wkt
287
+
288
+ if @db_type == 'array'
289
+ # in-Ruby point-in-polygon
290
+ @my_array.each do |geo_item|
291
+ GeoItem.centroid(geo_item)
292
+ geo_results << geo_item if geo_item.inside?(search_area)
293
+ end
294
+ elsif @db_type == 'mongodb'
295
+ polygon_gj = JSON.parse(search_area.to_geojson)['geometry']
296
+ cursor = @db_client.find(geo: { '$geoWithin' => { '$geometry' => polygon_gj } })
297
+ elsif @db_type == 'postgis'
298
+ cursor = @db_client.exec("SELECT id, ST_AsText(geom) AS geo, properties FROM mapplz AS start WHERE ST_Contains(ST_GeomFromText('#{wkt}'), start.geom)")
299
+ elsif @db_type == 'spatialite'
300
+ cursor = @db_client.exec("SELECT id, AsText(geom) AS geo, label FROM mapplz WHERE MBRContains(FromText('#{wkt}'), FromText(geom))")
301
+ end
302
+
303
+ geo_results += read_cursor(cursor)
304
+ end
305
+ geo_results
306
+ end
307
+
258
308
  def self.flip_path(path)
259
309
  path.map! do |pt|
260
310
  [pt[1].to_f, pt[0].to_f]
261
311
  end
262
312
  end
263
313
 
314
+ def self.parse_wkt(geo_item, geom_string)
315
+ if geom_string.index('POINT')
316
+ coordinates = geom_string.gsub('POINT', '').gsub('(', '').gsub(')', '').split(' ')
317
+ geo_item[:lat] = coordinates[1].to_f
318
+ geo_item[:lng] = coordinates[0].to_f
319
+ elsif geom_string.index('LINESTRING')
320
+ line_nodes = geom_string.gsub('LINESTRING', '').gsub('(', '').gsub(')', '').split(',')
321
+ geo_item[:path] = line_nodes.map do |pt|
322
+ pt = pt.split(' ')
323
+ [pt[1].to_f, pt[0].to_f]
324
+ end
325
+ elsif geom_string.index('POLYGON')
326
+ line_nodes = geom_string.gsub('POLYGON', '').gsub('(', '').gsub(')', '').split(', ')
327
+ geo_item[:path] = line_nodes.map do |pt|
328
+ pt = pt.split(' ')
329
+ [pt[1].to_f, pt[0].to_f]
330
+ end
331
+ end
332
+ geo_item
333
+ end
334
+
335
+ def self.standardize_geo(user_geo, lonlat = false, db = nil)
336
+ return [user_geo] if user_geo.is_a?(GeoItem)
337
+ geo_objects = []
338
+
339
+ if user_geo.is_a?(File)
340
+ file_type = File.extname(user_geo)
341
+ if ['.csv', '.tsv', '.tdv', '.txt', '.geojson', '.json'].include?(file_type)
342
+ # parse this as if it were sent as a string
343
+ user_geo = user_geo.read
344
+ else
345
+ # convert this with ogr2ogr and parse as a string
346
+ begin
347
+ `ogr2ogr -f "GeoJSON" tmp.geojson #{File.path(user_geo)}`
348
+ user_geo = File.open('tmp.geojson').read
349
+ rescue
350
+ raise 'gdal was not installed, or format was not accepted by ogr2ogr'
351
+ end
352
+ end
353
+ end
354
+
355
+ if user_geo.is_a?(String)
356
+ begin
357
+ user_geo = JSON.parse(user_geo)
358
+ rescue
359
+ # not JSON - attempt CSV
360
+ begin
361
+ CSV.parse(user_geo.gsub('\"', '""'), headers: true) do |row|
362
+ geo_objects += standardize_geo(row, lonlat, db)
363
+ end
364
+ return geo_objects
365
+ rescue
366
+ # not JSON or CSV - attempt mapplz parse
367
+ raise MapPLZException, 'call code() to parse mapplz language'
368
+ end
369
+ end
370
+ end
371
+
372
+ if user_geo.is_a?(Array) && user_geo.length > 0
373
+ if user_geo[0].is_a?(Array) && user_geo[0].length > 0
374
+ if user_geo[0][0].is_a?(Array) || (user_geo[0][0].is_a?(Hash) && user_geo[0][0].key?(:lat) && user_geo[0][0].key?(:lng))
375
+ # lines and shapes
376
+ user_geo.map! do |path|
377
+ path_pts = []
378
+ path.each do |path_pt|
379
+ if lonlat
380
+ lat = path_pt[1] || path_pt[:lat]
381
+ lng = path_pt[0] || path_pt[:lng]
382
+ else
383
+ lat = path_pt[0] || path_pt[:lat]
384
+ lng = path_pt[1] || path_pt[:lng]
385
+ end
386
+ path_pts << [lat, lng]
387
+ end
388
+
389
+ # polygon border repeats first point
390
+ if path_pts[0] == path_pts.last
391
+ geo_type = 'polygon'
392
+ path_pts = [path_pts]
393
+ else
394
+ geo_type = 'polyline'
395
+ end
396
+
397
+ geoitem = GeoItem.new(db)
398
+ geoitem[:path] = path_pts
399
+ geoitem[:type] = geo_type
400
+ geoitem
401
+ end
402
+ return user_geo
403
+ end
404
+ end
405
+
406
+ # multiple objects being added? iterate through
407
+ if user_geo[0].is_a?(Hash) || user_geo[0].is_a?(Array)
408
+ user_geo.each do |geo_piece|
409
+ geo_objects += standardize_geo(geo_piece, lonlat, db)
410
+ end
411
+ return geo_objects
412
+ end
413
+
414
+ # first two spots are a coordinate
415
+ validate_lat = user_geo[0].to_f != 0 || user_geo[0].to_s == '0'
416
+ validate_lng = user_geo[1].to_f != 0 || user_geo[1].to_s == '0'
417
+
418
+ if validate_lat && validate_lng
419
+ geo_object = GeoItem.new(db)
420
+ geo_object[:type] = 'point'
421
+
422
+ if lonlat
423
+ geo_object[:lat] = user_geo[1].to_f
424
+ geo_object[:lng] = user_geo[0].to_f
425
+ else
426
+ geo_object[:lat] = user_geo[0].to_f
427
+ geo_object[:lng] = user_geo[1].to_f
428
+ end
429
+ else
430
+ fail 'no latitude or longitude found'
431
+ end
432
+
433
+ # assume user properties are an ordered array of values known to the user
434
+ user_properties = user_geo.drop(2)
435
+
436
+ # only one property and it's a hash? it's a hash of properties
437
+ if user_properties.length == 1 && user_properties[0].is_a?(Hash)
438
+ user_properties[0].keys.each do |key|
439
+ geo_object[key.to_sym] = user_properties[0][key]
440
+ end
441
+ else
442
+ geo_object[:properties] = user_properties
443
+ end
444
+
445
+ geo_objects << geo_object
446
+
447
+ elsif user_geo.is_a?(Hash) || user_geo.is_a?(CSV::Row)
448
+ if user_geo.is_a?(CSV::Row)
449
+ # convert CSV::Row to geo hash
450
+ geo_hash = {}
451
+ user_geo.headers.each do |header|
452
+ geo_hash[header] = user_geo[header]
453
+ end
454
+ user_geo = geo_hash
455
+ end
456
+
457
+ # check for lat and lng
458
+ validate_lat = false
459
+ validate_lat = 'lat' if user_geo.key?('lat') || user_geo.key?(:lat)
460
+ validate_lat ||= 'latitude' if user_geo.key?('latitude') || user_geo.key?(:latitude)
461
+
462
+ validate_lng = false
463
+ validate_lng = 'lng' if user_geo.key?('lng') || user_geo.key?(:lng)
464
+ validate_lng ||= 'lon' if user_geo.key?('lon') || user_geo.key?(:lon)
465
+ validate_lng ||= 'long' if user_geo.key?('long') || user_geo.key?(:long)
466
+ validate_lng ||= 'longitude' if user_geo.key?('longitude') || user_geo.key?(:longitude)
467
+
468
+ if validate_lat && validate_lng
469
+ # single hash
470
+ geo_object = GeoItem.new(db)
471
+ geo_object[:lat] = user_geo[validate_lat].to_f
472
+ geo_object[:lng] = user_geo[validate_lng].to_f
473
+ geo_object[:type] = 'point'
474
+
475
+ user_geo.keys.each do |key|
476
+ next if key == validate_lat || key == validate_lng
477
+ geo_object[key.to_sym] = user_geo[key]
478
+ end
479
+ geo_objects << geo_object
480
+ elsif user_geo.key?('path') || user_geo.key?(:path)
481
+ # try line or polygon
482
+ path_pts = []
483
+ path = user_geo['path'] if user_geo.key?('path')
484
+ path = user_geo[:path] if user_geo.key?(:path)
485
+
486
+ if path_pts[0].is_a?(Array) && path_pts[0][0].is_a?(Array)
487
+ # ring polygon
488
+ path.each do |ring|
489
+ ring.map! do |path_pt|
490
+ if lonlat
491
+ lat = path_pt[1] || path_pt[:lat]
492
+ lng = path_pt[0] || path_pt[:lng]
493
+ else
494
+ lat = path_pt[0] || path_pt[:lat]
495
+ lng = path_pt[1] || path_pt[:lng]
496
+ end
497
+ [lat, lng]
498
+ end
499
+ end
500
+ path_pts = path
501
+ else
502
+ path.each do |path_pt|
503
+ if lonlat
504
+ lat = path_pt[1] || path_pt[:lat]
505
+ lng = path_pt[0] || path_pt[:lng]
506
+ else
507
+ lat = path_pt[0] || path_pt[:lat]
508
+ lng = path_pt[1] || path_pt[:lng]
509
+ end
510
+ path_pts << [lat, lng]
511
+ end
512
+
513
+ # polygon border repeats first point
514
+ if path_pts[0] == path_pts.last
515
+ geo_type = 'polygon'
516
+ path_pts = [path_pts]
517
+ else
518
+ geo_type = 'polyline'
519
+ end
520
+ end
521
+
522
+ geoitem = GeoItem.new(db)
523
+ geoitem[:path] = path_pts
524
+ geoitem[:type] = geo_type
525
+
526
+ property_list = user_geo.clone
527
+ property_list = property_list[:properties] if property_list.key?(:properties)
528
+ property_list = property_list['properties'] if property_list.key?('properties')
529
+ property_list.delete(:path)
530
+ property_list.keys.each do |prop|
531
+ geoitem[prop.to_sym] = property_list[prop]
532
+ end
533
+
534
+ geo_objects << geoitem
535
+ elsif user_geo.key?('geo') || user_geo.key?(:geo)
536
+ # this key is GeoJSON or WKT
537
+ geotext = (user_geo['geo'] || user_geo[:geo])
538
+ if geotext.upcase.index('POINT(') || geotext.upcase.index('LINESTRING(') || geotext.upcase.index('POLYGON(')
539
+ # try WKT
540
+ geoitem = GeoItem.new(db)
541
+ geoitem = MapPLZ.parse_wkt(geoitem, geotext)
542
+ else
543
+ # GeoJSON
544
+ begin
545
+ geoitem = standardize_geo(JSON.parse(geotext), lonlat, db)[0]
546
+ rescue
547
+ # did not recognize format
548
+ raise 'did not recognize format in CSV geo column'
549
+ end
550
+ end
551
+ user_geo.keys.each do |key|
552
+ next if ['lat', 'lng', 'geo', 'geom', 'geojson', 'wkt', :lat, :lng, :geo, :geom, :geojson, :wkt].include?(key)
553
+ geoitem[key.to_sym] = user_geo[key]
554
+ end
555
+ geo_objects << geoitem
556
+ else
557
+ # try GeoJSON
558
+ if user_geo.key?(:type)
559
+ user_geo['type'] = user_geo[:type] || ''
560
+ user_geo['features'] = user_geo[:features] if user_geo.key?(:features)
561
+ user_geo['properties'] = user_geo[:properties] || {}
562
+ if user_geo.key?(:geometry)
563
+ user_geo['geometry'] = user_geo[:geometry]
564
+ user_geo['geometry']['type'] = user_geo[:geometry][:type]
565
+ user_geo['geometry']['coordinates'] = user_geo[:geometry][:coordinates]
566
+ end
567
+ end
568
+ if user_geo.key?('type')
569
+ if user_geo['type'] == 'FeatureCollection' && user_geo.key?('features')
570
+ # recursive onto features
571
+ user_geo['features'].each do |feature|
572
+ geo_objects += standardize_geo(feature, lonlat, db)
573
+ end
574
+ elsif user_geo.key?('geometry') && user_geo['geometry'].key?('coordinates')
575
+ # each feature
576
+ coordinates = user_geo['geometry']['coordinates']
577
+
578
+ if user_geo['geometry']['type'] == 'Point'
579
+ geo_object = GeoItem.new(db)
580
+ geo_object[:lat] = coordinates[1].to_f
581
+ geo_object[:lng] = coordinates[0].to_f
582
+ geo_object[:type] = 'point'
583
+ geo_objects << geo_object
584
+ elsif user_geo['geometry']['type'] == 'LineString'
585
+ geo_object = GeoItem.new(db)
586
+ MapPLZ.flip_path(coordinates)
587
+ geo_object[:path] = coordinates
588
+ geo_object[:type] = 'polyline'
589
+ geo_objects << geo_object
590
+ elsif user_geo['geometry']['type'] == 'Polygon'
591
+ geo_object = GeoItem.new(db)
592
+ coordinates.map! do |ring|
593
+ MapPLZ.flip_path(ring)
594
+ end
595
+ geo_object[:path] = coordinates
596
+ geo_object[:type] = 'polygon'
597
+ geo_objects << geo_object
598
+ elsif user_geo['geometry']['type'] == 'MultiPoint'
599
+ coordinates.each do |point|
600
+ geo_object = GeoItem.new(db)
601
+ geo_object[:lat] = point[1].to_f
602
+ geo_object[:lng] = point[0].to_f
603
+ geo_object[:type] = 'point'
604
+ geo_objects << geo_object
605
+ end
606
+ elsif user_geo['geometry']['type'] == 'MultiLineString'
607
+ coordinates.each do |line|
608
+ geo_object = GeoItem.new(db)
609
+ geo_object[:path] = MapPLZ.flip_path(line)
610
+ geo_object[:type] = 'polyline'
611
+ geo_objects << geo_object
612
+ end
613
+ elsif user_geo['geometry']['type'] == 'MultiPolygon'
614
+ coordinates.each do |poly|
615
+ geo_object = GeoItem.new(db)
616
+ poly.map! do |ring|
617
+ MapPLZ.flip_path(ring)
618
+ end
619
+ geo_object[:path] = poly
620
+ geo_object[:type] = 'polygon'
621
+ geo_objects << geo_object
622
+ end
623
+ end
624
+
625
+ # store properties on all generated geometries
626
+ prop_keys = {}
627
+ if user_geo.key?('properties')
628
+ user_geo['properties'].keys.each do |key|
629
+ prop_keys[key.to_sym] = user_geo['properties'][key]
630
+ end
631
+ end
632
+ geo_objects.each do |geo|
633
+ prop_keys.keys.each do |key|
634
+ geo[key] = prop_keys[key]
635
+ end
636
+ end
637
+ end
638
+ end
639
+ end
640
+ end
641
+
642
+ geo_objects
643
+ end
644
+
264
645
  # alias methods
265
646
 
266
647
  # aliases for add
@@ -293,9 +674,14 @@ class MapPLZ
293
674
 
294
675
  private
295
676
 
677
+ # internal error record
678
+ class MapPLZException < Exception
679
+ end
680
+
296
681
  # internal map object record
297
682
  class GeoItem < Hash
298
- def initialize(db = { type: 'array', client: nil })
683
+ def initialize(db = nil)
684
+ db = { type: 'array', client: nil } if db.nil?
299
685
  @db = db
300
686
  @db_type = db[:type]
301
687
  @db_client = db[:client]
@@ -304,22 +690,28 @@ class MapPLZ
304
690
  def save!
305
691
  # update record in database
306
692
  if @db_type == 'mongodb'
307
- consistent_id = self[:_id]
308
- delete(:_id)
309
- @db[:client].update({ _id: BSON::ObjectId(consistent_id) }, self)
310
- self[:_id] = consistent_id
311
- elsif @db_type == 'postgis' || @db_type == 'spatialite'
693
+ save_obj = clone
694
+ save_obj.delete(:_id)
695
+ save_obj.delete(:lat)
696
+ save_obj.delete(:lng)
697
+ save_obj.delete(:path)
698
+ save_obj.delete(:centroid)
699
+ save_obj.delete(:type)
700
+ save_obj[:geo] = JSON.parse(to_geojson)['geometry']
701
+ @db[:client].update({ _id: BSON::ObjectId(self[:_id]) }, save_obj)
702
+ elsif @db_type == 'postgis'
703
+ geojson_props = (JSON.parse(to_geojson)['properties'] || {})
704
+ @db_client.exec("UPDATE mapplz SET geom = ST_GeomFromText('#{to_wkt}'), properties = '#{geojson_props.to_json}' WHERE id = #{self[:id]}") if @db_type == 'postgis'
705
+ elsif @db_type == 'spatialite'
312
706
  updaters = []
313
707
  keys.each do |key|
314
- next if [:id, :lat, :lng, :path, :type].include?(key)
708
+ next if [:id, :lat, :lng, :path, :type, :centroid].include?(key)
315
709
  updaters << "#{key} = '#{self[key]}'" if self[key].is_a?(String)
316
710
  updaters << "#{key} = #{self[key]}" if self[key].is_a?(Integer) || self[key].is_a?(Float)
317
711
  end
318
- updaters << "geom = ST_GeomFromText('#{to_wkt}')" if @db_type == 'postgis'
319
- updaters << "geom = AsText('#{to_wkt}')" if @db_type == 'spatialite'
712
+ updaters << "geom = AsText('#{to_wkt}')"
320
713
  if updaters.length > 0
321
- @db_client.exec("UPDATE mapplz SET #{updaters.join(', ')} WHERE id = #{self[:id]}") if @db_type == 'postgis'
322
- @db_client.execute("UPDATE mapplz SET #{updaters.join(', ')} WHERE id = #{self[:id]}") if @db_type == 'spatialite'
714
+ @db_client.execute("UPDATE mapplz SET #{updaters.join(', ')} WHERE id = #{self[:id]}")
323
715
  end
324
716
  end
325
717
  end
@@ -360,11 +752,12 @@ class MapPLZ
360
752
  if key?(:properties)
361
753
  property_list = { properties: self[:properties] }
362
754
  else
363
- property_list = self.clone
755
+ property_list = clone
364
756
  property_list.delete(:lat)
365
757
  property_list.delete(:lng)
366
758
  property_list.delete(:path)
367
759
  property_list.delete(:type)
760
+ property_list.delete(:centroid)
368
761
  end
369
762
 
370
763
  output_geo = {
@@ -386,13 +779,119 @@ class MapPLZ
386
779
  }
387
780
  elsif self[:type] == 'polygon'
388
781
  # polygon
782
+ rings = self[:path].clone
783
+ rings.map! do |ring|
784
+ MapPLZ.flip_path(ring)
785
+ end
389
786
  output_geo[:geometry] = {
390
787
  type: 'Polygon',
391
- coordinates: [MapPLZ.flip_path(self[:path])]
788
+ coordinates: rings
392
789
  }
393
790
  end
394
791
  output_geo.to_json
395
792
  end
793
+
794
+ def centroid
795
+ # verify up-to-date centroid exists
796
+ path_hash = Digest::SHA256.digest(self[:path].to_s)
797
+ (key?(:path) && key?(:centroid) && self[:centroid] == path_hash)
798
+ end
799
+
800
+ def distance_from(user_geo)
801
+ GeoItem.centroid(self)
802
+ user_geo = MapPLZ.standardize_geo(user_geo)[0]
803
+ GeoItem.centroid(user_geo)
804
+
805
+ Geokdtree::Tree.distance([user_geo[:lat], user_geo[:lng]], [self[:lat], self[:lng]])
806
+ end
807
+
808
+ def inside?(user_geo)
809
+ GeoItem.centroid(self)
810
+
811
+ # accept [point1, point2, point3, point1] as a polygon search area
812
+ if user_geo.is_a?(Array) && user_geo[0].is_a?(Array) && !user_geo[0][0].is_a?(Array)
813
+ user_geo = [user_geo]
814
+ end
815
+
816
+ user_geo = MapPLZ.standardize_geo(user_geo)[0]
817
+ path_pts = user_geo[:path]
818
+ path_pts = path_pts[0] if user_geo[:type] == 'polygon'
819
+
820
+ # point in polygon from http://jakescruggs.blogspot.com/2009/07/point-inside-polygon-in-ruby.html
821
+ c = false
822
+ i = -1
823
+ j = path_pts.size - 1
824
+ while (i += 1) < path_pts.size
825
+ if (path_pts[i][0] <= self[:lat] && self[:lat] < path_pts[j][0]) || (path_pts[j][0] <= self[:lat] && self[:lat] < path_pts[i][0])
826
+ if self[:lng] < (path_pts[j][1] - path_pts[i][1]) * (self[:lat] - path_pts[i][0]) / (path_pts[j][0] - path_pts[i][0]) + path_pts[i][1]
827
+ c = !c
828
+ end
829
+ end
830
+ j = i
831
+ end
832
+ c
833
+ end
834
+
835
+ def self.centroid(geo_item)
836
+ # generate centroid if possible and not already existing
837
+ if geo_item.key?(:path) && !geo_item.centroid
838
+ coordinates = geo_item[:path].clone
839
+
840
+ # centroid calculation from https://code.google.com/p/tokland/source/browse/trunk/centroid
841
+ consecutive_pairs = (coordinates + [coordinates.first]).each_cons(2)
842
+ area = 0.5 * consecutive_pairs.map do |(x0, y0), (x1, y1)|
843
+ (x0 * y1) - (x1 * y0)
844
+ end
845
+ area.inject(:+)
846
+
847
+ consecutive_pairs.map! do |(x0, y0), (x1, y1)|
848
+ cross = (x0 * y1 - x1 * y0)
849
+ [(x0 + x1) * cross, (y0 + y1) * cross]
850
+ end
851
+ (center_lat, center_lng) = consecutive_pairs.transpose.map do |cs|
852
+ cs.inject(:+) / (6 * area)
853
+ end
854
+
855
+ geo_item[:centroid] = Digest::SHA256.digest(geo_item[:path].to_s)
856
+ geo_item[:lat] = center_lat
857
+ geo_item[:lng] = center_lng
858
+ end
859
+ end
860
+ end
861
+
862
+ def read_cursor(cursor)
863
+ if cursor.nil?
864
+ []
865
+ else
866
+ geo_results = []
867
+ cursor.each do |geo_result|
868
+ if @db_type == 'postgis'
869
+ geo_item = GeoItem.new(@db)
870
+ geom = (geo_result['geo'] || geo_result[:geo]).upcase
871
+ geo_item = MapPLZ.parse_wkt(geo_item, geom)
872
+ geo_result = JSON.parse(geo_result['properties'])
873
+ elsif @db_type == 'spatialite'
874
+ geo_item = GeoItem.new(@db)
875
+ geom = (geo_result['geo'] || geo_result[:geo]).upcase
876
+ geo_item = MapPLZ.parse_wkt(geo_item, geom)
877
+ elsif @db_type == 'mongodb'
878
+ geom = { 'type' => 'Feature', 'geometry' => geo_result['geo'] }
879
+ geo_item = MapPLZ.standardize_geo(geom, true, @db)[0]
880
+ end
881
+
882
+ if geo_result.is_a?(Array)
883
+ geo_item[:properties] = geo_result
884
+ else
885
+ geo_result.keys.each do |key|
886
+ next if [:geo].include?(key.to_sym)
887
+ geo_item[key.to_sym] = geo_result[key]
888
+ end
889
+ end
890
+
891
+ geo_results << geo_item
892
+ end
893
+ geo_results
894
+ end
396
895
  end
397
896
 
398
897
  def code_line(index)
@@ -515,241 +1014,6 @@ class MapPLZ
515
1014
  code_line(index + 1)
516
1015
  end
517
1016
 
518
- def standardize_geo(user_geo, lonlat = false)
519
- geo_objects = []
520
-
521
- if user_geo.is_a?(String)
522
- begin
523
- user_geo = JSON.parse(user_geo)
524
- rescue
525
- # not JSON string - attempt mapplz parse
526
- return code(user_geo)
527
- end
528
- end
529
-
530
- if user_geo.is_a?(Array) && user_geo.length > 0
531
- if user_geo[0].is_a?(Array) && user_geo[0].length > 0
532
- if user_geo[0][0].is_a?(Array) || (user_geo[0][0].is_a?(Hash) && user_geo[0][0].key?(:lat) && user_geo[0][0].key?(:lng))
533
- # lines and shapes
534
- user_geo.map! do |path|
535
- path_pts = []
536
- path.each do |path_pt|
537
- if lonlat
538
- lat = path_pt[1] || path_pt[:lat]
539
- lng = path_pt[0] || path_pt[:lng]
540
- else
541
- lat = path_pt[0] || path_pt[:lat]
542
- lng = path_pt[1] || path_pt[:lng]
543
- end
544
- path_pts << [lat, lng]
545
- end
546
-
547
- # polygon border repeats first point
548
- if path_pts[0] == path_pts.last
549
- geo_type = 'polygon'
550
- else
551
- geo_type = 'polyline'
552
- end
553
-
554
- geoitem = GeoItem.new(@db)
555
- geoitem[:path] = path_pts
556
- geoitem[:type] = geo_type
557
- geoitem
558
- end
559
- return user_geo
560
- end
561
- end
562
-
563
- # multiple objects being added? iterate through
564
- if user_geo[0].is_a?(Hash) || user_geo[0].is_a?(Array)
565
- user_geo.each do |geo_piece|
566
- geo_objects += standardize_geo(geo_piece)
567
- end
568
- return geo_objects
569
- end
570
-
571
- # first two spots are a coordinate
572
- validate_lat = user_geo[0].to_f != 0 || user_geo[0].to_s == '0'
573
- validate_lng = user_geo[1].to_f != 0 || user_geo[1].to_s == '0'
574
-
575
- if validate_lat && validate_lng
576
- geo_object = GeoItem.new(@db)
577
- geo_object[:type] = 'point'
578
-
579
- if lonlat
580
- geo_object[:lat] = user_geo[1].to_f
581
- geo_object[:lng] = user_geo[0].to_f
582
- else
583
- geo_object[:lat] = user_geo[0].to_f
584
- geo_object[:lng] = user_geo[1].to_f
585
- end
586
- else
587
- fail 'no latitude or longitude found'
588
- end
589
-
590
- # assume user properties are an ordered array of values known to the user
591
- user_properties = user_geo.drop(2)
592
-
593
- # only one property and it's a hash? it's a hash of properties
594
- if user_properties.length == 1 && user_properties[0].is_a?(Hash)
595
- user_properties[0].keys.each do |key|
596
- geo_object[key.to_sym] = user_properties[0][key]
597
- end
598
- else
599
- geo_object[:properties] = user_properties
600
- end
601
-
602
- geo_objects << geo_object
603
-
604
- elsif user_geo.is_a?(Hash)
605
- # check for lat and lng
606
- validate_lat = false
607
- validate_lat = 'lat' if user_geo.key?('lat') || user_geo.key?(:lat)
608
- validate_lat ||= 'latitude' if user_geo.key?('latitude') || user_geo.key?(:latitude)
609
-
610
- validate_lng = false
611
- validate_lng = 'lng' if user_geo.key?('lng') || user_geo.key?(:lng)
612
- validate_lng ||= 'lon' if user_geo.key?('lon') || user_geo.key?(:lon)
613
- validate_lng ||= 'long' if user_geo.key?('long') || user_geo.key?(:long)
614
- validate_lng ||= 'longitude' if user_geo.key?('longitude') || user_geo.key?(:longitude)
615
-
616
- if validate_lat && validate_lng
617
- # single hash
618
- geo_object = GeoItem.new(@db)
619
- geo_object[:lat] = user_geo[validate_lat].to_f
620
- geo_object[:lng] = user_geo[validate_lng].to_f
621
- geo_object[:type] = 'point'
622
-
623
- user_geo.keys.each do |key|
624
- next if key == validate_lat || key == validate_lng
625
- geo_object[key.to_sym] = user_geo[key]
626
- end
627
- geo_objects << geo_object
628
- elsif user_geo.key?('path') || user_geo.key?(:path)
629
- # try line or polygon
630
- path_pts = []
631
- path = user_geo['path'] if user_geo.key?('path')
632
- path = user_geo[:path] if user_geo.key?(:path)
633
- path.each do |path_pt|
634
- if lonlat
635
- lat = path_pt[1] || path_pt[:lat]
636
- lng = path_pt[0] || path_pt[:lng]
637
- else
638
- lat = path_pt[0] || path_pt[:lat]
639
- lng = path_pt[1] || path_pt[:lng]
640
- end
641
- path_pts << [lat, lng]
642
- end
643
-
644
- # polygon border repeats first point
645
- if path_pts[0] == path_pts.last
646
- geo_type = 'polygon'
647
- else
648
- geo_type = 'polyline'
649
- end
650
-
651
- geoitem = GeoItem.new(@db)
652
- geoitem[:path] = path_pts
653
- geoitem[:type] = geo_type
654
-
655
- property_list = user_geo.clone
656
- property_list = property_list[:properties] if property_list.key?(:properties)
657
- property_list = property_list['properties'] if property_list.key?('properties')
658
- property_list.delete(:path)
659
- property_list.keys.each do |prop|
660
- geoitem[prop.to_sym] = property_list[prop]
661
- end
662
-
663
- geo_objects << geoitem
664
- else
665
- # try GeoJSON
666
- if user_geo.key?(:type)
667
- user_geo['type'] = user_geo[:type] || ''
668
- user_geo['features'] = user_geo[:features] if user_geo.key?(:features)
669
- user_geo['properties'] = user_geo[:properties] || {}
670
- if user_geo.key?(:geometry)
671
- user_geo['geometry'] = user_geo[:geometry]
672
- user_geo['geometry']['type'] = user_geo[:geometry][:type]
673
- user_geo['geometry']['coordinates'] = user_geo[:geometry][:coordinates]
674
- end
675
- end
676
- if user_geo.key?('type')
677
- if user_geo['type'] == 'FeatureCollection' && user_geo.key?('features')
678
- # recursive onto features
679
- user_geo['features'].each do |feature|
680
- geo_objects += standardize_geo(feature)
681
- end
682
- elsif user_geo.key?('geometry') && user_geo['geometry'].key?('coordinates')
683
- # each feature
684
- coordinates = user_geo['geometry']['coordinates']
685
-
686
- if user_geo['geometry']['type'] == 'Point'
687
- geo_object = GeoItem.new(@db)
688
- geo_object[:lat] = coordinates[1].to_f
689
- geo_object[:lng] = coordinates[0].to_f
690
- geo_object[:type] = 'point'
691
- geo_objects << geo_object
692
- elsif user_geo['geometry']['type'] == 'LineString'
693
- geo_object = GeoItem.new(@db)
694
- MapPLZ.flip_path(coordinates)
695
- geo_object[:path] = coordinates
696
- geo_object[:type] = 'polyline'
697
- geo_objects << geo_object
698
- elsif user_geo['geometry']['type'] == 'Polygon'
699
- geo_object = GeoItem.new(@db)
700
- coordinates.map! do |ring|
701
- MapPLZ.flip_path(ring)
702
- end
703
- geo_object[:path] = coordinates
704
- geo_object[:type] = 'polygon'
705
- geo_objects << geo_object
706
- elsif user_geo['geometry']['type'] == 'MultiPoint'
707
- coordinates.each do |point|
708
- geo_object = GeoItem.new(@db)
709
- geo_object[:lat] = point[1].to_f
710
- geo_object[:lng] = point[0].to_f
711
- geo_object[:type] = 'point'
712
- geo_objects << geo_object
713
- end
714
- elsif user_geo['geometry']['type'] == 'MultiLineString'
715
- coordinates.each do |line|
716
- geo_object = GeoItem.new(@db)
717
- geo_object[:path] = MapPLZ.flip_path(line)
718
- geo_object[:type] = 'polyline'
719
- geo_objects << geo_object
720
- end
721
- elsif user_geo['geometry']['type'] == 'MultiPolygon'
722
- coordinates.each do |poly|
723
- geo_object = GeoItem.new(@db)
724
- poly.map! do |ring|
725
- MapPLZ.flip_path(ring)
726
- end
727
- geo_object[:path] = poly
728
- geo_object[:type] = 'polygon'
729
- geo_objects << geo_object
730
- end
731
- end
732
-
733
- # store properties on all generated geometries
734
- prop_keys = {}
735
- if user_geo.key?('properties')
736
- user_geo['properties'].keys.each do |key|
737
- prop_keys[key.to_sym] = user_geo['properties'][key]
738
- end
739
- end
740
- geo_objects.each do |geo|
741
- prop_keys.keys.each do |key|
742
- geo[key] = prop_keys[key]
743
- end
744
- end
745
- end
746
- end
747
- end
748
- end
749
-
750
- geo_objects
751
- end
752
-
753
1017
  def parse_sql(where_clause, add_on = nil)
754
1018
  where_clause.downcase! unless where_clause.blank?
755
1019
  where_clause = where_clause.gsub('?', '\'?\'') if add_on.present?
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mapplz
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.3
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nick Doiron
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2014-07-10 00:00:00.000000000 Z
11
+ date: 2014-07-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rspec