UNPKG

21.5 kBtext/coffeescriptView Raw
1geolib = require 'geolib'
2csv = require 'fast-csv'
3
4MapPLZ = ->
5
6MapPLZ.standardize = (geo, props) ->
7 result = new MapItem
8 if typeof geo.lat != "undefined" && typeof geo.lng != "undefined"
9 result.lat = geo.lat * 1
10 result.lng = geo.lng * 1
11 else if geo.path
12 result.path = geo.path
13 result.properties = props || {}
14 result
15
16MapPLZ.prototype.add = (param1, param2, param3, param4) ->
17 this.mapitems = [] unless this.mapitems
18 this.database = null unless this.database
19
20 callback = (err, items) ->
21 callback = param2 if typeof param2 == "function"
22 callback = param3 if typeof param3 == "function"
23 callback = param4 if typeof param4 == "function"
24
25 # try for lat, lng point
26 lat = param1 * 1
27 lng = param2 * 1
28 unless isNaN(lat) || isNaN(lng)
29 pt = MapPLZ.standardize({ lat: lat, lng: lng })
30 pt.type = "point"
31
32 if param3 != null
33 # allow JSON string of properties
34 if typeof param3 == 'string'
35 try
36 pt.properties = JSON.parse param3
37 catch
38
39 if this.isArray param3
40 # array
41 pt.properties = { property: param3 }
42 else if typeof param3 == 'object'
43 # object
44 for key in Object.keys(param3)
45 pt.properties[key] = param3[key]
46 else if typeof param3 != 'function'
47 # string, number, or other singular property
48 pt.properties = { property: param3 }
49
50 this.save pt, callback
51 return
52
53 if typeof param1 == 'string'
54 # try JSON parse
55 try
56 param1 = JSON.parse param1
57 if this.isGeoJson param1
58 result = this.addGeoJson param1, callback
59 this.save result, callback if result
60 return
61 catch
62 if param1[param1.length-1] == ')' && (param1.indexOf 'POINT' == 0 || param1.indexOf 'LINESTRING' == 0 || param1.indexOf 'POLYGON' == 0)
63 # try WKT
64 contents = ''
65 if param1.indexOf('POINT') == 0
66 contents = param1.replace('POINT', '').replace('(', '').replace(')', '').trim().split(' ').reverse()
67 else if param1.indexOf('LINESTRING') == 0
68 pts = param1.replace('LINESTRING', '').replace('(', '').replace(')', '').split(',')
69 contents = []
70 for pt in pts
71 contents.push pt.trim().split(' ').reverse()
72 else if param1.indexOf('POLYGON') == 0
73 pts = param1.replace('POLYGON', '').replace('(', '').replace(')', '').replace('(', '').replace(')', '').split(',')
74 contents = []
75 for pt in pts
76 contents.push pt.trim().split(' ').reverse()
77
78 this.add contents, param2 || callback, callback
79 return
80
81 if (param1.indexOf('map') > -1) and (param1.indexOf('plz') > -1)
82 # try mapplz code
83 this.process_code param1, callback
84 return
85
86 else
87 # try CSV
88 callbacks = 0
89 contents = []
90 mapstore = this
91
92 records = 0
93 finished = false
94
95 csv.fromString(param1, { headers: true })
96 .on('record', (data) ->
97 records++
98 if data.geo || data.geom || data.wkt
99 mapstore.add (data.geo || data.geom || data.wkt), data, (err, item) ->
100 contents.push item unless err
101 callback(err, contents) if finished && contents.length == records
102 else
103 mapstore.add data, (err, item) ->
104 contents.push item unless err
105 callback(err, contents) if finished && contents.length == records
106 )
107 .on('end', ->
108 finished = true
109 callback(null, contents) if contents.length == records
110 )
111 return
112
113 if this.isArray param1
114 # param1 is an array
115
116 if param1.length >= 2
117 lat = param1[0] * 1
118 lng = param1[1] * 1
119 unless isNaN(lat) || isNaN(lng)
120 result = MapPLZ.standardize { lat: lat, lng: lng }
121 result.type = 'point'
122 for prop in param1.slice(2)
123 if typeof prop == 'string'
124 try
125 prop = JSON.parse prop
126 catch
127 prop = prop
128
129 if typeof prop == 'object'
130 for key in Object.keys(prop)
131 result.properties[key] = prop[key]
132 else if typeof prop != 'function'
133 result.properties = { property: prop }
134
135 this.save result, callback
136 return
137
138 if typeof param1[0] == 'object'
139 if this.isArray param1[0]
140 # param1 contains an array of arrays - probably coordinates
141 if this.isArray param1[0][0]
142 # polygon
143 result = MapPLZ.standardize({ path: param1 })
144 result.type = 'polygon'
145 else
146 # line
147 result = MapPLZ.standardize({ path: param1 })
148 result.type = 'line'
149
150 if result && param2
151 # try JSON parsing
152 if typeof param2 == 'string'
153 try
154 param2 = JSON.parse param2
155 catch
156 param2 = param2
157
158 if typeof param2 == 'object'
159 if this.isArray param2
160 result.properties = { property: param2 }
161 else
162 result.properties = param2
163 else if typeof param2 != 'function'
164 result.properties = { property: param2 }
165
166 this.save result, callback
167 return
168 else
169 # param1 contains an array of objects to add
170 results = []
171 for obj in param1
172 results.push this.add(obj)
173 this.save results, callback
174 return
175
176
177 else if typeof param1 == 'object'
178 # regular object
179 if param1.lat && param1.lng
180 result = MapPLZ.standardize({ lat: param1.lat, lng: param1.lng })
181 result.type = "point"
182 for key in Object.keys(param1)
183 result.properties[key] = param1[key] unless key == 'lat' || key == 'lng'
184 this.save result, callback
185 return
186
187 else if param1.path
188 result = MapPLZ.standardize({ path: param1.path })
189 if typeof param1.path[0][0] == 'object'
190 result.type = 'polygon'
191 else
192 result.type = 'line'
193 for key in Object.keys(param1)
194 result.properties[key] = param1[key] unless key == 'path'
195
196 this.save result, callback
197 return
198
199 else if this.isGeoJson param1
200 results = this.addGeoJson param1, callback
201 this.save results, callback if results
202 return
203
204MapPLZ.prototype.count = (query, callback) ->
205 if this.database
206 this.database.count(query, callback)
207 else
208 this.query query, (err, results) ->
209 callback(err, results.length)
210
211MapPLZ.prototype.query = (query, callback) ->
212 if this.database
213 this.database.query(query, callback)
214 else
215 if query == null || query == ""
216 callback(null, this.mapitems)
217 else
218 results = []
219 for mapitem in this.mapitems
220 if mapitem && mapitem.type != "deleted"
221 match = true
222 for key of query
223 match = (mapitem.properties[key] == query[key])
224 break unless match
225 results.push mapitem if match
226 callback(null, results)
227
228MapPLZ.prototype.where = (query, callback) ->
229 this.query(query, callback)
230
231MapPLZ.prototype.near = (neargeo, count, callback) ->
232 nearpt = neargeo
233 if this.isArray(neargeo)
234 nearpt = new MapItem
235 nearpt.type = "point"
236 nearpt.lat = neargeo[0]
237 nearpt.lng = neargeo[1]
238 else
239 if typeof neargeo == 'string'
240 neargeo = JSON.parse(neargeo)
241 unless typeof neargeo.lat == "undefined" && typeof neargeo.lng == "undefined"
242 nearpt = this.addGeoJson(neargeo)
243
244 if this.database
245 this.database.near(nearpt, count, callback)
246 else
247 this.query "", (err, items) ->
248 centerpts = []
249 for item in items
250 center = item.center()
251 centerpts.push { latitude: center.lat, longitude: center.lng }
252 centerpts = geolib.orderByDistance({ latitude: nearpt.lat, longitude: nearpt.lng }, centerpts)
253 for pt, p in centerpts
254 centerpts[p] = items[p]
255 break if p >= count
256 callback(null, centerpts.slice(0, count))
257
258MapPLZ.prototype.inside = (withingeo, callback) ->
259 withinpoly = withingeo
260 if this.isArray(withingeo)
261 withinpoly = new MapItem
262 withinpoly.type = "polygon"
263 withinpoly.path = withingeo
264 else
265 if typeof withingeo == 'string'
266 withingeo = JSON.parse(withingeo)
267 unless withingeo.path
268 withinpoly = this.addGeoJson(withingeo)
269
270 if this.database
271 this.database.inside(withinpoly, callback)
272 else
273 this.query "", (err, items) ->
274 withinpoly = withinpoly.path[0]
275 for pt, p in withinpoly
276 withinpoly[p] = { latitude: pt[0], longitude: pt[1] }
277 results = []
278 for item in items
279 center = item.center()
280 if geolib.isPointInside({ latitude: center.lat, longitude: center.lng }, withinpoly)
281 results.push item
282 callback(null, results)
283
284MapPLZ.prototype.save = (items, callback) ->
285 if this.database
286 if this.isArray(items)
287 for item in items
288 item.database = this.database
289 item.save(callback)
290 else
291 items.database = this.database
292 items.save(callback)
293 else
294 if this.isArray(items)
295 for item in items
296 item.mapitems = this.mapitems
297 this.mapitems.concat items
298 else
299 items.mapitems = this.mapitems
300 this.mapitems.push items
301 callback(null, items)
302
303MapPLZ.prototype.isArray = (inspect) ->
304 return (typeof inspect == 'object' && typeof inspect.push == 'function')
305
306MapPLZ.prototype.isGeoJson = (json) ->
307 type = json.type
308 return (type && (type == "Feature" || type == "FeatureCollection"))
309
310MapPLZ.prototype.addGeoJson = (gj, callback) ->
311 if gj.type == "FeatureCollection"
312 results = []
313 that = this
314 iter_callback = (feature_index) ->
315 if feature_index < gj.features.length
316 feature = that.addGeoJson(gj.features[feature_index])
317 that.save feature, (err, saved) ->
318 results.push saved
319 iter_callback(feature_index + 1)
320 else
321 callback(null, results)
322 iter_callback(0)
323
324 else if gj.type == "Feature"
325 geom = gj.geometry
326 result = ""
327 if geom.type == "Point"
328 result = MapPLZ.standardize({ lat: geom.coordinates[1], lng: geom.coordinates[0] })
329 result.type = "point"
330 else if geom.type == "LineString"
331 result = MapPLZ.standardize({ path: this.reverse_path(geom.coordinates) })
332 result.type = "line"
333 else if geom.type == "Polygon"
334 result = MapPLZ.standardize({ path: [this.reverse_path(geom.coordinates[0])] })
335 result.type = "polygon"
336
337 result.properties = gj.properties || {}
338 result
339
340MapPLZ.prototype.process_code = (code, callback) ->
341 code_lines = code.split("\n")
342 code_level = "toplevel"
343 button_layers = []
344 code_button = 0
345 code_layers = []
346 code_label = ""
347 code_color = null
348 code_latlngs = []
349
350 finish_add = ->
351 added = 0
352 for item in code_layers
353 item.database = this.database
354 item.save (err) ->
355 added++
356 callback(err, code_layers) if code_layers.length == added
357
358 code_line = (index) ->
359 if index >= code_lines.length
360 return finish_add()
361
362 line = code_lines[index].trim()
363 codeline = line.toLowerCase().split(' ')
364
365 if code_level == 'toplevel'
366 code_level = 'map' if line.indexOf('map') > -1
367 return code_line(index + 1)
368
369 else if code_level == 'map' || code_level == 'button'
370 if codeline.indexOf('button') > -1 || codeline.indexOf('btn') > -1
371 code_level = 'button'
372 button_layers.push { layers: [] }
373 code_button = button_layers.length
374
375 if codeline.indexOf('marker') > -1
376 code_level = 'marker'
377 code_latlngs = []
378 return code_line(index + 1)
379
380 else if codeline.indexOf('line') > -1
381 code_level = 'line'
382 code_latlngs = []
383 return code_line(index + 1)
384
385 else if codeline.indexOf('shape') > -1
386 code_level = 'shape'
387 code_latlngs = []
388 return code_line(index + 1)
389
390 if codeline.indexOf('plz') > -1 || codeline.indexOf('please') > -1
391 if code_level == 'map'
392 code_level = 'toplevel'
393 return finish_add()
394
395 else if code_level == 'button'
396 # add button
397 code_level = 'map'
398 code_button = nil
399 return code_line(index + 1)
400
401 else if code_level == 'marker' || code_level == 'line' || code_level == 'shape'
402 if codeline.indexOf('plz') > -1 || codeline.indexOf('please') > -1
403
404 if code_level == 'marker'
405 geoitem = new MapItem
406 geoitem.lat = code_latlngs[0][0]
407 geoitem.lng = code_latlngs[0][1]
408 geoitem.properties = { label: (code_label || '') }
409 code_layers.push geoitem
410
411 else if code_level == 'line'
412 geoitem = new MapItem
413 geoitem.path = code_latlngs
414 geoitem.properties = {
415 color: (code_color || ''),
416 label: (code_label || '')
417 }
418 code_layers.push geoitem
419
420 else if code_level == 'shape'
421 geoitem = new MapItem
422 geoitem.path = [code_latlngs]
423 geoitem.properties = {
424 color: (code_color || ''),
425 fill_color: (code_color || ''),
426 label: (code_label || '')
427 }
428 code_layers.push geoitem
429
430 if code_button
431 code_level = 'button'
432 else
433 code_level = 'map'
434
435 code_latlngs = []
436 return code_line(index + 1)
437
438 # geocoding starts with @ - disabled
439
440 # reading a color
441 if codeline[0].indexOf('#') == 0
442 code_color = codeline.trim()
443 if code_color.length != 4 && code_color.length != 7
444 # named color
445 code_color = code_color.replace('#', '')
446
447 return code_line(index + 1)
448
449 # reading a raw string (probably text for a popup)
450 if codeline[0].indexOf('"') == 0
451 # check button
452 code_label = line.substring( line.indexOf('"') + 1 )
453 code_label = code_label.substring(0, code_label.indexOf('"') - 1)
454
455 # reading a latlng coordinate
456 if line.indexOf('[') > -1 && line.indexOf(',') > -1 && line.indexOf(']') > -1
457 latlng_line = line.replace('[', '').replace(']', '').split(',')
458 latlng_line[0] *= 1
459 latlng_line[1] *= 1
460
461 # must be a 2D coordinate
462 return code_line(index + 1) if latlng_line.length != 2
463
464 code_latlngs.push latlng_line
465
466 return code_line(index + 1)
467
468 code_line(index + 1)
469 code_line(0)
470
471MapPLZ.reverse_path = (path) ->
472 path_pts = path.slice(0)
473 for p, pt in path_pts
474 path_pts[pt] = path_pts[pt].slice(0).reverse()
475 path_pts
476
477## MapPLZ data is returned as MapItems
478
479MapItem = ->
480MapItem.prototype.toGeoJson = ->
481 if this.type == "point"
482 gj_geo = { type: "Point", coordinates: [this.lng, this.lat] }
483 else if this.type == "line"
484 linepath = MapPLZ.reverse_path(this.path)
485 gj_geo = { type: "LineString", coordinates: linepath }
486 else if this.type == 'polygon'
487 polypath = [MapPLZ.reverse_path(this.path[0])]
488 gj_geo = { type: "Polygon", coordinates: polypath }
489 JSON.stringify { type: "Feature", geometry: gj_geo, properties: this.properties }
490
491MapItem.prototype.toWKT = ->
492 if this.type == "point"
493 "POINT(#{this.lng} #{this.lat})"
494 else if this.type == "line"
495 linepath = MapPLZ.reverse_path(this.path)
496 for p, pt in linepath
497 linepath[pt] = linepath[pt].join ' '
498 linepath = linepath.join ', '
499 "LINESTRING(#{linepath})"
500 else if this.type == 'polygon'
501 polypath = MapPLZ.reverse_path(this.path[0])
502 for p, pt in polypath
503 polypath[pt] = polypath[pt].join ' '
504 polypath = polypath.join ', '
505 "POLYGON((#{polypath}))"
506
507MapItem.prototype.save = (callback) ->
508 if this.database
509 my_mapitem = this
510 this.database.save this, (err, id) ->
511 my_mapitem.id = id if id
512 callback(err, my_mapitem)
513 else
514 callback(null, this)
515
516MapItem.prototype.delete = (callback) ->
517 if this.database
518 my_mapitem = this
519 this.database.delete this, (err) ->
520 callback(err)
521 else
522 if this.mapitems.indexOf(this) > -1
523 this.mapitems.splice(this.mapitems.indexOf(this), 1)
524 this.type = "deleted"
525 callback(null)
526
527MapItem.prototype.center = ->
528 if this.type == "point"
529 { lat: this.lat, lng: this.lng }
530 else if this.type == "line"
531 avg = { lat: 0, lng: 0 }
532 for pt in this.path
533 avg.lat += pt[0]
534 avg.lng += pt[1]
535 avg.lat /= this.path.length
536 avg.lng /= this.path.length
537 avg
538 else if this.type == "polygon"
539 avg = { lat: 0, lng: 0 }
540 for pt in this.path[0]
541 avg.lat += pt[0]
542 avg.lng += pt[1]
543 avg.lat /= this.path[0].length
544 avg.lng /= this.path[0].length
545 avg
546
547
548## Database Drivers
549
550## PostGIS Database Driver
551
552PostGIS = ->
553PostGIS.prototype.save = (item, callback) ->
554 if item.id
555 this.client.query "UPDATE mapplz SET geom = ST_GeomFromText('#{item.toWKT()}'), properties = '#{JSON.stringify(item.properties)}' WHERE id = #{item.id * 1}", (err, result) ->
556 console.error err if err
557 callback(err, item.id)
558 else
559 this.client.query "INSERT INTO mapplz (properties, geom) VALUES ('#{JSON.stringify(item.properties)}', ST_GeomFromText('#{item.toWKT()}')) RETURNING id", (err, result) ->
560 console.error err if err
561 callback(err, result.rows[0].id || null)
562
563PostGIS.prototype.delete = (item, callback) ->
564 this.client.query "DELETE FROM mapplz WHERE id = #{item.id * 1}", (err) ->
565 item = null
566 callback(err)
567
568PostGIS.prototype.count = (query, callback) ->
569 condition = "1=1"
570 if query && query.length
571 condition = query
572 where_prop = condition.trim().split(' ')[0]
573 condition = condition.replace(where_prop, "json_extract_path_text(properties, '#{where_prop}')")
574
575 this.client.query "SELECT COUNT(*) AS count FROM mapplz WHERE #{condition}", (err, result) ->
576 callback(err, result.rows[0].count || null)
577
578PostGIS.prototype.processResults = (err, db, result, callback) ->
579 if err
580 console.error err
581 callback(err, [])
582 else
583 results = []
584 for row in result.rows
585 geo = JSON.parse(row.geo)
586 result = MapPLZ.prototype.addGeoJson { type: "Feature", geometry: geo, properties: row.properties }
587 result.id = row.id
588 result.database = db
589 results.push result
590 callback(err, results)
591
592PostGIS.prototype.query = (query, callback) ->
593 condition = "1=1"
594 db = this
595 if query && query.length
596 condition = query
597 where_prop = condition.trim().split(' ')[0]
598 condition = condition.replace(where_prop, "json_extract_path_text(properties, '#{where_prop}')")
599
600 this.client.query "SELECT ST_AsGeoJSON(geom) AS geo, properties FROM mapplz WHERE #{condition}", (err, result) ->
601 db.processResults(err, db, result, callback)
602
603PostGIS.prototype.near = (nearpt, count, callback) ->
604 db = this
605 this.client.query "SELECT id, ST_AsGeoJSON(geom) AS geo, properties, ST_Distance(start.geom::geography, ST_GeomFromText('#{nearpt.toWKT()}')::geography) AS distance FROM mapplz AS start ORDER BY distance LIMIT #{count}", (err, results) ->
606 db.processResults(err, db, results, callback)
607
608PostGIS.prototype.inside = (withinpoly, callback) ->
609 db = this
610 this.client.query "SELECT id, ST_AsGeoJSON(geom) AS geo, properties FROM mapplz AS start WHERE ST_Contains(ST_GeomFromText('#{withinpoly.toWKT()}'), start.geom)", (err, results) ->
611 db.processResults(err, db, results, callback)
612
613## MongoDB Database Driver
614
615MongoDB = ->
616MongoDB.prototype.save = (item, callback) ->
617 saveobj = {}
618 saveobj = JSON.parse(JSON.stringify(item.properties)) if item.properties
619 saveobj.geo = JSON.parse(item.toGeoJson()).geometry
620 if item.id
621 saveobj._id = item.id
622 this.collection.save saveobj, (err) ->
623 console.error err if err
624 callback(err, item.id)
625 else
626 this.collection.insert saveobj, (err, results) ->
627 console.error err if err
628 result = results[0] || null
629 callback(err, result._id || null)
630
631MongoDB.prototype.delete = (item, callback) ->
632 this.collection.remove { _id: item.id }, (err) ->
633 item = null
634 callback(err)
635
636MongoDB.prototype.count = (query, callback) ->
637 condition = query || {}
638 this.collection.find query, (err, cursor) ->
639 if err
640 console.error err
641 callback(err, null)
642 else
643 cursor.count (err, count) ->
644 callback(err, count || 0)
645
646MongoDB.prototype.query = (query, callback) ->
647 condition = query || {}
648 db = this
649 this.collection.find(query).toArray (err, rows) ->
650 if err
651 console.error err
652 callback(err, [])
653 else
654 results = []
655 for row in rows
656 excluded = {}
657 for key of row
658 if key != "_id" && key != "geom"
659 excluded[key] = row[key]
660 result = MapPLZ.prototype.addGeoJson { type: "Feature", geometry: row.geo, properties: excluded }
661 result.id = row._id
662 result.database = db
663 results.push result
664 callback(err, results)
665
666MongoDB.prototype.near = (nearpt, count, callback) ->
667 max = 40010000000
668 nearquery = {
669 geo: {
670 $nearSphere: {
671 $geometry: JSON.parse(nearpt.toGeoJson()).geometry,
672 }
673 }
674 }
675
676 this.query nearquery, (err, results) ->
677 callback(err, (results || []).slice(0, count))
678
679MongoDB.prototype.inside = (withinpoly, callback) ->
680 withinquery = {
681 geo: {
682 $geoWithin: {
683 $geometry: JSON.parse(withinpoly.toGeoJson()).geometry
684 }
685 }
686 }
687 this.query withinquery, callback
688
689## get it working with Node.js!
690
691if exports
692 exports.MapPLZ = MapPLZ
693 exports.MapItem = MapItem
694 exports.PostGIS = PostGIS
695 exports.MongoDB = MongoDB