utils_common.js

import Producer from '../producer'
import Consumer from '../consumer'
import Exchange from '../exchange'
import Queue from '../queue'
import Binding from '../binding'
import Scene from '../scene'
import Timer from '../timer'
import { displayProducer } from './producer'
import { displayConsumer } from './consumer'
import { displayExchange } from './exchange'
import { displayQueue } from './queue'
import { displayBinding } from './binding'

/**
 * Calculates the point on the line that's nearest to the mouse position.
 *
 * https://stackoverflow.com/questions/24043967/detect-if-mouse-is-over-an-object-inside-canvas
 * @param {object} line - line with start and end point (x1,y1,x2,y2)
 * @param {number} x - x position
 * @param {number} y - y position
 * @returns {object}
 */
const linepointNearestMouse = (line, x, y) => {
  const lerp = (a, b, c) => a + c * (b - a)
  const dx = line.x1 - line.x0
  const dy = line.y1 - line.y0
  const t = ((x - line.x0) * dx + (y - line.y0) * dy) / (dx * dx + dy * dy)
  const lineX = lerp(line.x0, line.x1, t)
  const lineY = lerp(line.y0, line.y1, t)
  return {
    x: lineX,
    y: lineY
  }
}

/**
 * Creates the topology shown on the canvas.
 * A new scene with timer plus the parts defined in the configuration.
 *
 * @param {object} ctx - canvas context
 * @param {object} conf - configuration as JSON
 */
const createTopology = (ctx, conf) => {
  window.scene = new Scene(ctx, window.innerWidth, 450)
  if (conf.description) {
    window.scene.description = conf.description
  }
  window.timer = new Timer(window.scene)

  const exchanges = []
  const queues = []

  if (conf.exchanges) {
    conf.exchanges.forEach((exchange) => {
      const newExchange = new Exchange(
        exchange.x,
        exchange.y,
        exchange.name,
        exchange.type
      )
      newExchange.addToScene(window.scene)
      exchanges.push(newExchange)
    })
    // alternate exchanges
    const alternateExchanges = conf.exchanges.filter((s) => s.alternate)
    const currentExchanges = window.scene.getObjectsInScene('Exchange')
    alternateExchanges.forEach((ex) => {
      const e = currentExchanges.filter((s) => s.name == ex.name)[0]
      const a = currentExchanges.filter((s) => s.name == ex.alternate)[0]
      e.setAlternate(a)
    })
  }
  if (conf.queues) {
    conf.queues.forEach((queue) => {
      const newQueue = new Queue(
        queue.x,
        queue.y,
        queue.name,
        queue.ttl,
        exchanges[queue.dlx],
        queue.dlxrk,
        queue.maxLength
      )
      newQueue.addToScene(window.scene)
      queues.push(newQueue)
    })
  }
  if (conf.producers) {
    conf.producers.forEach((producer) => {
      const newProducer = new Producer(producer.x, producer.y, producer.name)
      const currentExchanges = window.scene.getObjectsInScene('Exchange')
      for (const val in producer.publishes) {
        currentExchanges.forEach((ex) => {
          if (ex.name === producer.publishes[val].exchange) {
            newProducer.addMessageToExchange(
              ex,
              producer.publishes[val].routingKey,
              producer.publishes[val].message
            )
          }
        })
      }
      newProducer.addToScene(window.scene)
    })
  }
  if (conf.consumers) {
    conf.consumers.forEach((consumer) => {
      const newConsumer = new Consumer(
        consumer.x,
        consumer.y,
        consumer.name,
        consumer.consumes,
        consumer.mode
      )
      consumer.consumes.forEach((val) => {
        newConsumer.addQueue(queues[val])
      })
      newConsumer.addToScene(window.scene)
    })
  }
  if (conf.bindings) {
    conf.bindings.forEach((binding) => {
      const newBinding = new Binding(
        exchanges[binding.exchange],
        queues[binding.queue],
        binding.routingKey
      )
      newBinding.addToScene(window.scene)
    })
  }

  window.scene.render()
}

/**
 * Adds the CSS class to the selected one and remove it from all other ones.
 *
 * @param {string} form
 */
const displayForm = (form) => {
  const formsCollection = document.getElementsByTagName('form')
  for (let i = 0; i < formsCollection.length; i += 1) {
    document
      .querySelector(`#${formsCollection[i].id}`)
      .parentNode.parentNode.classList.remove('panel-wrap-out')
    if (`${form.toLowerCase()}Form` === formsCollection[i].id) {
      document
        .querySelector(`#${formsCollection[i].id}`)
        .parentNode.parentNode.classList.add('panel-wrap-out')
    }
  }
}

/**
 * Decides which component have to be displayed to get created.
 *
 * @param {object} e - Event object
 */
const addNewComponent = (e) => {
  e.preventDefault()
  e.stopPropagation()
  displayForm(e.target.value)
  switch (e.target.value) {
    case 'Producer':
      displayProducer()
      break
    case 'Consumer':
      displayConsumer()
      break
    case 'Exchange':
      displayExchange()
      break
    case 'Queue':
      displayQueue()
      break
    case 'Binding':
      displayBinding()
      break
    default:
      break
  }
  e.target.selectedIndex = 0
}

/**
 * Current mouse position inside of a cirle.
 * @param {object} val - can be Exchange or Queue object
 * @param {number} mx - x position of the mouse
 * @param {number} my - y position of the mouse
 * @returns {object}
 */
const findCircle = (val, mx, my) => {
  let found
  if (val.constructor.name === 'Exchange' || val.constructor.name === 'Queue') {
    const d = Math.floor(Math.sqrt((val.x - mx) ** 2 + (val.y - my) ** 2))
    if (d <= val.radius) {
      found = val
    }
  }
  return found
}

/**
 * Current mouse position inside of a square.
 * @param {object} val - can be Producer or Consumer object
 * @param {number} mx - x position of the mouse
 * @param {number} my - y position of the mouse
 * @returns {object}
 */
const findSquare = (val, mx, my) => {
  let found
  if (
    val.constructor.name === 'Producer' ||
    val.constructor.name === 'Consumer'
  ) {
    if (
      mx >= val.x &&
      mx <= val.x + val.width &&
      my >= val.y &&
      my <= val.y + val.height
    ) {
      found = val
    }
  }
  return found
}

/**
 * Current mouse position over a line.
 * @param {Binding} val - Binding object
 * @param {number} mx - x position of the mouse
 * @param {number} my - y position of the mouse
 * @returns {object}
 */
const findLine = (val, mx, my) => {
  let found
  const line = {
    x0: val.x1,
    x1: val.x2,
    y0: val.y1,
    y1: val.y2
  }

  let x1
  let x2
  if (line.x0 > line.x1) {
    x1 = line.x1
    x2 = line.x0
  } else {
    x1 = line.x0
    x2 = line.x1
  }
  if (mx > x1 && mx < x2) {
    // determine how close the mouse must be to the line
    // for the mouse to be inside the line
    const tolerance = 3
    const linepoint = linepointNearestMouse(line, mx, my)
    const dx = mx - linepoint.x
    const dy = my - linepoint.y
    const distance = Math.abs(Math.sqrt(dx * dx + dy * dy))
    if (distance < tolerance) {
      found = val
    }
  }
  return found
}

/**
 * Find the actor in the scene from the current mouse position.
 * @param {object} e - Event object
 * @param {*} line
 * @returns {object} - undefined or the found actor in scene
 */
const findPosition = (e, line = false) => {
  const mx = e.clientX - e.target.offsetLeft
  const my = e.clientY - e.target.offsetTop
  let obj
  window.scene.actors.forEach((val) => {
    const foundProducerConsumer = findSquare(val, mx, my)
    if (foundProducerConsumer) {
      obj = foundProducerConsumer
    }
    const foundExchangeQueue = findCircle(val, mx, my)
    if (foundExchangeQueue) {
      obj = foundExchangeQueue
    }
    if (line) {
      const foundLine = findLine(val, mx, my)
      if (foundLine) {
        obj = foundLine
      }
    }
  })
  return obj
}

export {
  linepointNearestMouse,
  createTopology,
  displayForm,
  addNewComponent,
  findCircle,
  findSquare,
  findLine,
  findPosition
}