217 lines
8.3 KiB
CoffeeScript
217 lines
8.3 KiB
CoffeeScript
###* @preserve OverlappingMarkerSpiderfier
|
|
https://github.com/jawj/OverlappingMarkerSpiderfier-Leaflet
|
|
Copyright (c) 2011 - 2012 George MacKerron
|
|
Released under the MIT licence: http://opensource.org/licenses/mit-license
|
|
Note: The Leaflet maps API must be included *before* this code
|
|
###
|
|
|
|
# NB. string literal properties -- object['key'] -- are for Closure Compiler ADVANCED_OPTIMIZATION
|
|
|
|
(->
|
|
return unless this['L']? # return from wrapper func without doing anything
|
|
|
|
class @['OverlappingMarkerSpiderfier']
|
|
p = @:: # this saves a lot of repetition of .prototype that isn't optimized away
|
|
p['VERSION'] = '0.2.6'
|
|
|
|
twoPi = Math.PI * 2
|
|
|
|
p['keepSpiderfied'] = no # yes -> don't unspiderfy when a marker is selected
|
|
p['nearbyDistance'] = 20 # spiderfy markers within this range of the one clicked, in px
|
|
|
|
p['circleSpiralSwitchover'] = 9 # show spiral instead of circle from this marker count upwards
|
|
# 0 -> always spiral; Infinity -> always circle
|
|
p['circleFootSeparation'] = 25 # related to circumference of circle
|
|
p['circleStartAngle'] = twoPi / 12
|
|
p['spiralFootSeparation'] = 28 # related to size of spiral (experiment!)
|
|
p['spiralLengthStart'] = 11 # ditto
|
|
p['spiralLengthFactor'] = 5 # ditto
|
|
|
|
p['legWeight'] = 1.5
|
|
p['legColors'] =
|
|
'usual': '#222'
|
|
'highlighted': '#f00'
|
|
|
|
# Note: it's OK that this constructor comes after the properties, because of function hoisting
|
|
constructor: (@map, opts = {}) ->
|
|
(@[k] = v) for own k, v of opts
|
|
@initMarkerArrays()
|
|
@listeners = {}
|
|
@map.addEventListener(e, => @['unspiderfy']()) for e in ['click', 'zoomend']
|
|
|
|
p.initMarkerArrays = ->
|
|
@markers = []
|
|
@markerListeners = []
|
|
|
|
p['addMarker'] = (marker) ->
|
|
return @ if marker['_oms']?
|
|
marker['_oms'] = yes
|
|
markerListener = => @spiderListener(marker)
|
|
marker.addEventListener('click', markerListener)
|
|
@markerListeners.push(markerListener)
|
|
@markers.push(marker)
|
|
@ # return self, for chaining
|
|
|
|
p['getMarkers'] = -> @markers[0..] # returns a copy, so no funny business
|
|
|
|
p['removeMarker'] = (marker) ->
|
|
@['unspiderfy']() if marker['_omsData']? # otherwise it'll be stuck there forever!
|
|
i = @arrIndexOf(@markers, marker)
|
|
return @ if i < 0
|
|
markerListener = @markerListeners.splice(i, 1)[0]
|
|
marker.removeEventListener('click', markerListener)
|
|
delete marker['_oms']
|
|
@markers.splice(i, 1)
|
|
@ # return self, for chaining
|
|
|
|
p['clearMarkers'] = ->
|
|
@['unspiderfy']()
|
|
for marker, i in @markers
|
|
markerListener = @markerListeners[i]
|
|
marker.removeEventListener('click', markerListener)
|
|
delete marker['_oms']
|
|
@initMarkerArrays()
|
|
@ # return self, for chaining
|
|
|
|
# available listeners: click(marker), spiderfy(markers), unspiderfy(markers)
|
|
p['addListener'] = (event, func) ->
|
|
(@listeners[event] ?= []).push(func)
|
|
@ # return self, for chaining
|
|
|
|
p['removeListener'] = (event, func) ->
|
|
i = @arrIndexOf(@listeners[event], func)
|
|
@listeners[event].splice(i, 1) unless i < 0
|
|
@ # return self, for chaining
|
|
|
|
p['clearListeners'] = (event) ->
|
|
@listeners[event] = []
|
|
@ # return self, for chaining
|
|
|
|
p.trigger = (event, args...) ->
|
|
func(args...) for func in (@listeners[event] ? [])
|
|
|
|
p.generatePtsCircle = (count, centerPt) ->
|
|
circumference = @['circleFootSeparation'] * (2 + count)
|
|
legLength = circumference / twoPi # = radius from circumference
|
|
angleStep = twoPi / count
|
|
for i in [0...count]
|
|
angle = @['circleStartAngle'] + i * angleStep
|
|
new L.Point(centerPt.x + legLength * Math.cos(angle),
|
|
centerPt.y + legLength * Math.sin(angle))
|
|
|
|
p.generatePtsSpiral = (count, centerPt) ->
|
|
legLength = @['spiralLengthStart']
|
|
angle = 0
|
|
for i in [0...count]
|
|
angle += @['spiralFootSeparation'] / legLength + i * 0.0005
|
|
pt = new L.Point(centerPt.x + legLength * Math.cos(angle),
|
|
centerPt.y + legLength * Math.sin(angle))
|
|
legLength += twoPi * @['spiralLengthFactor'] / angle
|
|
pt
|
|
|
|
p.spiderListener = (marker) ->
|
|
markerSpiderfied = marker['_omsData']?
|
|
@['unspiderfy']() unless markerSpiderfied and @['keepSpiderfied']
|
|
if markerSpiderfied
|
|
@trigger('click', marker)
|
|
else
|
|
nearbyMarkerData = []
|
|
nonNearbyMarkers = []
|
|
pxSq = @['nearbyDistance'] * @['nearbyDistance']
|
|
markerPt = @map.latLngToLayerPoint(marker.getLatLng())
|
|
for m in @markers
|
|
continue unless @map.hasLayer(m)
|
|
mPt = @map.latLngToLayerPoint(m.getLatLng())
|
|
if @ptDistanceSq(mPt, markerPt) < pxSq
|
|
nearbyMarkerData.push(marker: m, markerPt: mPt)
|
|
else
|
|
nonNearbyMarkers.push(m)
|
|
if nearbyMarkerData.length is 1 # 1 => the one clicked => none nearby
|
|
@trigger('click', marker)
|
|
else
|
|
@spiderfy(nearbyMarkerData, nonNearbyMarkers)
|
|
|
|
p.makeHighlightListeners = (marker) ->
|
|
highlight: => marker['_omsData'].leg.setStyle(color: @['legColors']['highlighted'])
|
|
unhighlight: => marker['_omsData'].leg.setStyle(color: @['legColors']['usual'])
|
|
|
|
p.spiderfy = (markerData, nonNearbyMarkers) ->
|
|
@spiderfying = yes
|
|
numFeet = markerData.length
|
|
bodyPt = @ptAverage(md.markerPt for md in markerData)
|
|
footPts = if numFeet >= @['circleSpiralSwitchover']
|
|
@generatePtsSpiral(numFeet, bodyPt).reverse() # match from outside in => less criss-crossing
|
|
else
|
|
@generatePtsCircle(numFeet, bodyPt)
|
|
spiderfiedMarkers = for footPt in footPts
|
|
footLl = @map.layerPointToLatLng(footPt)
|
|
nearestMarkerDatum = @minExtract(markerData, (md) => @ptDistanceSq(md.markerPt, footPt))
|
|
marker = nearestMarkerDatum.marker
|
|
leg = new L.Polyline [marker.getLatLng(), footLl], {
|
|
color: @['legColors']['usual']
|
|
weight: @['legWeight']
|
|
clickable: no
|
|
}
|
|
@map.addLayer(leg)
|
|
marker['_omsData'] = {usualPosition: marker.getLatLng(), leg: leg}
|
|
unless @['legColors']['highlighted'] is @['legColors']['usual']
|
|
mhl = @makeHighlightListeners(marker)
|
|
marker['_omsData'].highlightListeners = mhl
|
|
marker.addEventListener('mouseover', mhl.highlight)
|
|
marker.addEventListener('mouseout', mhl.unhighlight)
|
|
marker.setLatLng(footLl)
|
|
marker.setZIndexOffset(marker.options.zIndexOffset + 1000000)
|
|
marker
|
|
delete @spiderfying
|
|
@spiderfied = yes
|
|
@trigger('spiderfy', spiderfiedMarkers, nonNearbyMarkers)
|
|
|
|
p['unspiderfy'] = (markerNotToMove = null) ->
|
|
return @ unless @spiderfied?
|
|
@unspiderfying = yes
|
|
unspiderfiedMarkers = []
|
|
nonNearbyMarkers = []
|
|
for marker in @markers
|
|
if marker['_omsData']?
|
|
@map.removeLayer(marker['_omsData'].leg)
|
|
marker.setLatLng(marker['_omsData'].usualPosition) unless marker is markerNotToMove
|
|
marker.setZIndexOffset(marker.options.zIndexOffset - 1000000)
|
|
mhl = marker['_omsData'].highlightListeners
|
|
if mhl?
|
|
marker.removeEventListener('mouseover', mhl.highlight)
|
|
marker.removeEventListener('mouseout', mhl.unhighlight)
|
|
delete marker['_omsData']
|
|
unspiderfiedMarkers.push(marker)
|
|
else
|
|
nonNearbyMarkers.push(marker)
|
|
delete @unspiderfying
|
|
delete @spiderfied
|
|
@trigger('unspiderfy', unspiderfiedMarkers, nonNearbyMarkers)
|
|
@ # return self, for chaining
|
|
|
|
p.ptDistanceSq = (pt1, pt2) ->
|
|
dx = pt1.x - pt2.x
|
|
dy = pt1.y - pt2.y
|
|
dx * dx + dy * dy
|
|
|
|
p.ptAverage = (pts) ->
|
|
sumX = sumY = 0
|
|
for pt in pts
|
|
sumX += pt.x; sumY += pt.y
|
|
numPts = pts.length
|
|
new L.Point(sumX / numPts, sumY / numPts)
|
|
|
|
p.minExtract = (set, func) -> # destructive! returns minimum, and also removes it from the set
|
|
for item, index in set
|
|
val = func(item)
|
|
if ! bestIndex? || val < bestVal
|
|
bestVal = val
|
|
bestIndex = index
|
|
set.splice(bestIndex, 1)[0]
|
|
|
|
p.arrIndexOf = (arr, obj) ->
|
|
return arr.indexOf(obj) if arr.indexOf?
|
|
(return i if o is obj) for o, i in arr
|
|
-1
|
|
).call(this)
|