[Web] 使用 Cytoscape.js 實現廠房機台地圖繪製功能

使用 Cytoscape.js 實現廠房機台地圖繪製功能

前言


故事緣起是來自一個製造業客戶,目前既有功能是可以檢視廠房平面圖及機台的相對位置,可使用顏色標記出有狀況的機台,並且在點選機台時顯示該機台的即時資訊。

客戶的痛點在於該地圖及機台是使用 HTML Table 刻出,也就表示如果機台位置異動必須經由軟體工程師調整,無法讓工廠管理者自行維護處理;另外工程師在面對以 HTML Table 刻出的地圖及機台時,若要調整相對位置或平移數個機台是非常傷神的,所以筆者嘗試使用 Cytoscape.js 來解決這些痛點。

 

 

可行性驗證 (POC)


由於筆者以前也沒有使用過 Cytoscape.js 類別庫的經驗,只知道是一個視覺化類別庫,因此先針對以下幾點功能進行可行性驗證:

  • 可否新增不同大小形狀的物件(機台)
  • 可否拖拉移動物件(機台)擺放在所需的位置
  • 可否刪除物件(機台)
  • 是否可以儲存 / 取出目前顯示的所有物件(機台)
  • 是否可以點對點的連線(牆壁)
  • 是否可在點選時取得物件(機台)資料
  • 是否可主動依照物件(機台)ID動態調整該物件(機台)背景色

 

 

首先產生畫板


從 npm 下載 cytoscape 套件。

npm install cytoscape

 

引入專案中

import cytoscape from 'cytoscape';

 

針對 idcydiv 建立出畫板,並儲存在 this.cy;其中 style 定義了 node 樣式,而樣式皆可經由新增 node 時給予的 data 參數來決定,因此可產生不同風格的 node 物件。

this.cy = cytoscape({
  container: document.getElementById('cy'),
  elements: [
    {
      group: 'nodes',
      data: { name: 'Test', weight: 75, type: 'circle', width: 30, height: 30 },
      position: { x: 50, y: 100 }
    }
  ],
  layout: {
    name: 'preset'
  },
  style: [
    {
      selector: 'node',
      style: {
        'label': 'data(name)',
        "shape": "data(type)",
        'width': "data(width)",
        'height': "data(height)",
        'font-size': 12,
      }
    }
  ]
})

 

顯示一下成果,畫布已經產生,並且擁有一個剛剛在初始時加入 elementstest node 物件。

 

 

POC - 新增不同大小形狀的物件


可以透過 cy.add() 加入 node 物件,可在 data 中自訂寬高 width / height 及形狀 type,若沒有賦予 id 值時會自動產生出 guid 作為該物件的 id 值。

handleAddMachine01 = () => {
  this.cy.add({
    group: 'nodes',
    data: { name: 'M', weight: 75, type: 'rectangle', width: 30, height: 50 },
    position: { x: 50, y: 100 }
  })
}

handleAddMachine02 = () => {
  this.cy.add({
    group: 'nodes',
    data: { name: 'P', weight: 75, type: 'rectangle', width: 80, height: 50 },
    position: { x: 150, y: 100 }
  })
}

 

從以下結果可以發現確實可以依照需求產生不同大小形狀的物件。

 

 

POC - 拖拉移動物件(機台)擺放在所需的位置


可以直接透過滑鼠點選物件後拖拉物件移動,也可以按 Ctrl 及滑鼠左鍵圈選多個需要移動的物件後,一次性地移動相對位置。

 

 

POC - 刪除物件(機台)


可以透 cy.$(':selected') 取得被選取的物件,然後透過 remove() 將這些物件移除。

handleRemove = () => {
  const eles = this.cy.$(':selected');
  eles.remove()
}

 

 

POC - 儲存 / 取出目前顯示的所有物件


可以透 cy.json() 取出資料後暫存 (此範例僅保存在 localStorage 中);當需要重現資料時,可以透過 cy.json(data) 將資料放入後顯示。

handleSave = () => {
  // 儲存目前的畫面資料
  const cyjsonStr = JSON.stringify(this.cy.json())
  window.localStorage.setItem("elements", cyjsonStr);
  message.success('This layout has been Saved.');
}

handleRestore = () => {
  // 移除畫面上所有物件
  this.cy.elements().remove();

  // 取出畫面資料顯示在畫板上
  const cyjson = JSON.parse(window.localStorage.getItem("elements"))
  this.cy.json({ ...cyjson })
}

 

調整物件位置後按下儲存,關閉頁籤並再次開啟時,可經由 Restore 取出資料呈現剛才儲存的所有物件。

 

 

POC - 點對點的連線(牆壁)


目前 Cytoscape.js 已有許多成熟的 extension 可以輔助我們完成各項功能,其中 cytoscape-edgehandles 可以幫忙我們完成畫線功能,因此先透過 npm 安裝該套件。

npm install cytoscape-edgehandles

 

引入專案並向 cytoscape 註冊使用此插件。

import edgehandles from 'cytoscape-edgehandles'
cytoscape.use(edgehandles);

 

初始 edgehandles 插件並存入 this.eh 變數中,在 edgehandles(default) 方法中可以加入設定值來調整預設值(此範例先使用預設值無需傳入資料),細節請參考官方設定。

this.eh = this.cy.edgehandles();

 

由於畫線的過程中會有一些樣式的套用,因此需要加入一些 style 上去,所以在剛剛建立 cytoscape 畫板的 style 清單中補上以下樣式。

{
  selector: '.eh-handle',
  style: {
    'background-color': 'red',
    'width': 12,
    'height': 12,
    'shape': 'ellipse',
    'overlay-opacity': 0,
    'border-width': 12, // makes the handle easier to hit
    'border-opacity': 0
  }
},
{
  selector: '.eh-hover',
  style: {
    'background-color': 'red'
  }
},
{
  selector: '.eh-source',
  style: {
    'border-width': 2,
    'border-color': 'red'
  }
},
{
  selector: '.eh-target',
  style: {
    'border-width': 2,
    'border-color': 'red'
  }
},
{
  selector: '.eh-preview, .eh-ghost-edge',
  style: {
    'background-color': 'red',
    'line-color': 'red',
    'target-arrow-color': 'red',
    'source-arrow-color': 'red'
  }
},
{
  selector: '.eh-ghost-edge.eh-preview-active',
  style: {
    'opacity': 0
  }
}

 

接著就可以測試看看了。當游標移上物件時,會出現紅色的 indicator 表示可連線狀態,接著點下滑鼠左鍵拖曳就可以完成連線,以此作為平面圖的牆面。

 

若一直保持可連線的狀況,對於想移動物件時是種阻礙,因此最好是設定一個開關當要用的時候在開啟即可;我們可以透過 eh.enable()eh.disable() 切換,另外紅色的 indicator 有可能會殘留,因此在 disable 時可以呼叫 eh.hide() 先隱藏起來 (在儲存前也可以先執行此方法來避免 indicator 也被存入)。

handleSwitchConnectMode = () => {
  
  // 可以開啟或關閉node連線模式,避免在拖曳的時候影響使用者體驗
  const { isEnableEh } = this.state

  if (isEnableEh) {
    this.eh.hide()
    this.eh.disable();
  } else {
    this.eh.enable();
  }

  this.setState({ isEnableEh: !isEnableEh })

}

 

 

POC - 點選時取得物件(機台)資料


要取得物件資料前,要先定義物件被點選時的事件,透過 cy.on() 訂定 nodeclick 的事件,從中取得該 node 的資料;以下代碼會將 node id 存放在 state 中顯示在畫面上。

const self = this
this.cy.on('click', 'node', function (evt) {
  var node = evt.target;
  const nodeData = node.json()
  self.setState({ selectedNodeId: nodeData.data.id })
});

 

在點選物件時,右下方會顯示該物件 ID,在實務應用上可經由此 ID 延伸向後端取得該機台的即時資訊。

 

 

POC - 主動依照物件 ID 動態調整該物件背景色


在實際應用情境上,可能會定期從中台取得機台狀態,並要 highlight 有狀況的機台,因此需主動依據機台編號調整物件背景色,所以先定義一組高亮的 style 吧。

{
  selector: '.highlight',
  style: {
    'background-color': '#ffd118',
    'line-color': '#ffd118',
    'target-arrow-color': '#ffd118',
    'transition-property': 'background-color, line-color, target-arrow-color',
    'transition-duration': '0.5s'
  }
}

 

為了測試方便,我們從 cy.json() 把所有機台物件都列出。

handleFindMe = () => {

  const source = this.cy.json()
  if (source) {
    const { elements: { nodes: nodes } } = source
    if (nodes && nodes.length > 0) {
      // 取出所有機台物件
      const machines = nodes.filter(node => node.data.type === "rectangle").map(n => n.data)
      this.setState({ machines: machines })
      
      // 顯示清單視窗
      this.showFindMeModal()
    }
  }
}

 

選定特定物件 ID 後透過 cy.animate() 動畫效果移動畫布讓將該物件置中顯示,再針對該物件套用特定樣式;由於此範例的作用僅在於吸引眼球的注意,因此透過 flashClass() 暫時切換樣式,等固定時間後自動復原。

handleFindMeModalOk = e => {

  const eleId = this.state.selectedNodeId
  if (eleId) {
    // 將該物件至於畫布中顯示
    this.cy.animate({ center: { eles: `#${eleId}` } })
    
    // 套用 class 固定時間後復原
    this.cy.$(`#${eleId}`).flashClass('highlight', 1500);
  }

};

 

效果如下

 

 

後記


站在巨人的肩膀進行開發確實可以減少許多技術面上的問題,讓我們專心地聚焦在應用層面上,本文僅記錄筆者從發想到執行 POC 的過程,目前尚未實際將此 solution 應用於專案中,未來若有機會實際應用再來分享需要特別注意的地方吧!

有興趣的朋友可以至 DEMO 網站玩玩。

 

 

參考資訊


Cytoscape.js

cytoscape.js-edgehandles

cytoscape.js-undo-redo

 

 

 


希望此篇文章可以幫助到需要的人

若內容有誤或有其他建議請不吝留言給筆者喔 !