mapplz 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
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