• Jump To … +
    algorithm.coffee as.dat.gui.js data.coffee data.tile.js fbui.coffee mouse.coffee nlmouse.coffee rgbtohsl.coffee
  • mouse.coffee

  • ¶

    A NetLogo-like mouse handler. See: addEventListener

    u = ABM.Util
  • ¶

    Event delegation in a nutshell:

    1) DOM event handlers set the current state of the mouse object (e.g. @down and @moved) 2) @computeEventTypes calculates event names based on the current mouse state (e.g. ‘mousemove’ and ‘dragstart’), and sets additional state (e.g. @dragging) 3) @delegateEventsToAllAgents finds agents beneath the the mouse and fires the calculated events on their agentsets 4) the triggered agentsets call the appropriate registered callbacks (if any), with a mouseEvent as an argument

    class ABM.Mouse
  • ¶

    Create and start mouse obj, args: a model, and a callback method.

      constructor: (@model, @callback) ->
        @lastX = Infinity; @lastY = Infinity
        @div = @model.div
        @lastAgentsHovered = []
        @draggingAgents = []
        @divOffset = @model.div.getBoundingClientRect()
        @start()
  • ¶

    Start/stop the mouseListeners. Note that NetLogo’s model is to have mouse move events always on, rather than starting/stopping them on mouse down/up. We may want do make that optional, using the more standard down/up enabling move events.

      start: -> # Note: multiple calls safe
        @div.addEventListener("mousedown", @handleMouseDown, false)
        document.body.addEventListener("mouseup", @handleMouseUp, false)
        @div.addEventListener("mousemove", @handleMouseMove, false)
        @model.on('step', @handleStep)
        @lastX=@lastY=@x=@y=@pixX=@pixY=NaN; @moved=@down=false
      stop: -> # Note: multiple calls safe
        @div.removeEventListener("mousedown", @handleMouseDown, false)
        document.body.removeEventListener("mouseup", @handleMouseUp, false)
        @div.removeEventListener("mousemove", @handleMouseMove, false)
        @model.off('step', @handleStep)
        @lastX=@lastY=@x=@y=@pixX=@pixY=NaN; @moved=@down=false
  • ¶

    Handlers for eventListeners. Each handler modifies state properties on the mouse object, like @down and @moved, then calls the generic @handleMouseEvent.

      handleMouseDown: (e) =>
        @down = true
        @moved = false
        @handleMouseEvent(e)
      handleMouseUp: (e) =>
        @down = false
        @moved = false
        @handleMouseEvent(e)
      handleMouseMove: (e) =>
        @setXY(e)
        @moved = true
        @handleMouseEvent(e)
  • ¶

    Check for mouseover and mouseout events whenever the model steps, e.g. in case agents move beneath a stationary mouse

      handleStep: () =>
        @delegateMouseOverAndOutEvents(@x, @y) if not isNaN(@x)
  • ¶

    This is the entry point for most of the event work. All event handlers above lead here.

      handleMouseEvent: (e) =>
  • ¶

    First we calculate what events we need to fire off.

        eventTypes = @computeEventTypes()
  • ¶

    Then we fire them off to agents underneath the mouse.

        @delegateEventsToAllAgents(eventTypes, e)
  • ¶

    And lastly we run any generic callbacks that the user has registered. This should probably be removed–it’s leftover from the old mouse module.

        @callback(e) if @callback?
    
      getEventPos: (e) ->
  • ¶

    modified from http://www.quirksmode.org/js/events_properties.html previously used e.offsetX, e.offsetY, which was less robust (failed in tiledroplets.html, for example)

        xPos = 0
        yPos = 0
        if (e.pageX or e.pageY)
          xPos = e.pageX
          yPos = e.pageY
        else if (e.clientX or e.clientY)
          xPos = e.clientX + document.body.scrollLeft + document.documentElement.scrollLeft
          yPos = e.clientY + document.body.scrollTop + document.documentElement.scrollTop
        return { x: xPos - @divOffset.left, y: yPos - @divOffset.top }
    
      setXY: (e) ->
        eventPos = @getEventPos(e)
        @lastX = @x; @lastY = @y
        @pixX = eventPos.x; @pixY = eventPos.y
        [@x, @y] = @model.patches.pixelXYtoPatchXY(@pixX,@pixY)
        @dx = @lastX - @x; @dy = @lastY - @y
  • ¶

    This is where we determine which mouse events to fire given the current mouse state. We also edit mouse state to remember if the mouse is dragging.

  • ¶

    The current supported events are: ‘mousedown’, ‘mouseup’, ‘dragstart’, ‘drag’, ‘dragend’, ‘mousemove’, ‘mouseover’, and ‘mouseout’ These events seem to cover most use cases.

  • ¶

    Note that ‘drag’, ‘dragend’, ‘mouseover’, and ‘mouseout’ are computed separately in delegateDragEvents and delegateMouseOverAndOutEvents.

      computeEventTypes: () =>
        eventTypes = []
    
        if @down and not @moved
          eventTypes.push 'mousedown'
    
        if not @down and not @moved
          eventTypes.push 'mouseup'
    
        if @down and @moved
          if not @dragging
            eventTypes.push 'dragstart'
          @dragging = true
    
        if not @down and @dragging
          @dragging = false
          @dragEnd = true
    
        if @moved
          eventTypes.push 'mousemove'
    
        return eventTypes
  • ¶

    This is the entry point for event delegation. Delegation just means letting an agentset know that a certain kind of mouse event happened.

  • ¶

    Importantly, we allow ‘mouseover’ and’ ‘mouseout’ to trigger on several agentsets at once. All other mouse events can only happen to one agentset at a time. This means that a single ‘mousedown’ event will only ever trigger a single callback.

      delegateEventsToAllAgents: (types, e) ->
  • ¶

    First we try to delegate the mouse events to turtles

        delegatedAgent = @delegateEventsToTurtlesAtPoint(types, @x, @y, e)
  • ¶

    If there aren’t any turtles to alert, we move on to links

        if not delegatedAgent
          delegatedAgent = @delegateEventsToLinksAtPoint(types, @x, @y, e)
  • ¶

    If there aren’t any links to alert, we move on to patches

        if not delegatedAgent
          @delegateEventsToPatchAtPoint(types, @x, @y, e)
  • ¶

    Drag events only get delegated to turtles

        @delegateDragEvents(@x, @y, e)
  • ¶

    Mouseover and mouseout events get delegated to all patches, links and turtles beneath the mouse

        @delegateMouseOverAndOutEvents(@x, @y, e)
    
      delegateEventsToPatchAtPoint: (eventTypes, x, y, e) ->
        curPatch = @model.patches.patch(x, y)
        @emitAgentEvent(type, curPatch, @mouseEvent(curPatch, e)) for type in eventTypes
    
      delegateEventsToTurtlesAtPoint: (eventTypes, x, y, e) ->
  • ¶

    We use patches for a convenient performance boost, like a poor-man’s quadtree

        curPatch = @model.patches.patch(x, y)
  • ¶

    iterate through all turtles in this patch and its neighbors

        for patch in curPatch.n.concat(curPatch)
          for turtle in patch.turtlesHere()
            if turtle.hitTest(x, y)
              @emitAgentEvent(type, turtle, @mouseEvent(turtle, e)) for type in eventTypes
              return turtle
    
      delegateEventsToLinksAtPoint: (eventTypes, x, y, e) ->
        for link in @model.links
          if link.hitTest(x, y)
            mouseEvent = @mouseEvent(link, e)
            @emitAgentEvent(type, link, mouseEvent) for type in eventTypes
            return link
    
      emitAgentEvent: (eventType, agent, mouseEvent) ->
  • ¶

    We keep track of agents that start getting dragged so that we can later easily emit ‘drag’ events

        if eventType == 'dragstart'
          @draggingAgents.push(agent)
        agent.breed.emit(eventType, mouseEvent)
        if agent.breed.mainSet?
          agent.breed.mainSet.emit(eventType, mouseEvent)
    
      delegateDragEvents: (x, y, e) =>
        for agent in @draggingAgents
          mouseEvent = @mouseEvent(agent, e)
          if @moved then @emitAgentEvent('drag', agent, mouseEvent)
          if @dragEnd then @emitAgentEvent('dragend', agent, mouseEvent)
        if @dragEnd
          @draggingAgents = []
          @dragEnd = false
    
      delegateMouseOverAndOutEvents: (x, y, e) =>
        agentsHere = {}
        agents = []
        curPatch = @model.patches.patch(x, y)
    
        agents = u.clone(@model.links)
        for patch in curPatch.n.concat(curPatch)
          agents = agents.concat(patch.turtlesHere())
  • ¶

    mouseover

        for agent in agents
          if agent.hitTest(x, y)
            agentsHere[agent.breed.name] ?= {}
            agentsHere[agent.breed.name][agent.id] = agent
            if (not @lastAgentsHovered[agent.breed.name] or agent.id not of @lastAgentsHovered[agent.breed.name])
              @emitAgentEvent('mouseover', agent, @mouseEvent(agent, e))
  • ¶

    mouseout

        for breedname of @lastAgentsHovered
          for agentId of @lastAgentsHovered[breedname]
            if (not agentsHere[breedname] or agentId not of agentsHere[breedname])
              agent = @lastAgentsHovered[breedname][agentId]
              @emitAgentEvent('mouseout', agent, @mouseEvent(agent, e))
    
        @lastAgentsHovered = agentsHere
  • ¶

    The mouseEvent is what is passed to the callback registered with agentset.on(). It contains a reference to the affected agent, the patch coord where the event occured, the change in mouse position, and the original DOM event.

      mouseEvent: (agent, e) ->
        return {target: agent, patchX: @x, patchY: @y, dx: @dx, dy: @dy, originalEvent: e}