aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

Visual Parts SDK を使ってフリートマップを作ってみた

f:id:aptpod-tetsu:20210425000532j:plain

こんにちは。Visual M2M Data Visualizer の開発を担当している白金です。

この度、Visual M2M Data Visualizer Ver3.0.0 のアップデートとあわせて 可視化用パーツ「ビジュアルパーツ」を開発するための開発キット(以下「Visual Parts SDK」) をリリースしました。

Visual Parts SDKを使用して可視化用パーツをカスタマイズ開発することで、Visual M2M Data Visualizerに、ユーザー様自身やパートナー企業様の手で新しい可視化方法を追加することが可能になります。

早速、Visual Parts SDKを使ってフリートマップのビジュアルパーツを作ってみたので Visual Parts SDK とあわせて紹介したいと思います。

f:id:aptpod_tech-writer:20210423163058p:plain
作成したフリートマップのビジュアルパーツ

Visual Parts SDK の紹介

Visual Parts SDK とは

Visual M2M Data Visualizer (以下「Data Visualizer」) に表示する可視化パーツをカスタマイズ開発するためのSDKです。

Visual Parts SDK により、ユーザー様自身やパートナー企業様のご要望に対して Data Visualizer に含まれている標準ビジュアルパーツで表現が難しかった可視化方法が、カスタマイズ開発することで解決することが可能になります。

f:id:aptpod_tech-writer:20210423115104p:plain

Visual Parts SDKの構成

Visual Parts SDK は JavaScript の言語をサポートしている 以下2つのnpmパッケージで構成しています。

@aptpod/data-viz-visual-parts-sdk
(APIライブラリー)

Visual Parts SDKの本体です。Data VisualizerのAPIを提供するライブラリーです。

@aptpod/data-viz-create-visual-parts-react
(ワークスペース作成ツール)

ビジュアルパーツの開発に使用するワークスペース(開発用のディレクトリ)を作成するためのパッケージスクリプトです。ビジュアルパーツを開発する際に、このスクリプトを使用することは必須ではありませんが、使用するとTypeScript, React, Styled Components などのフレームワークも活用して効率的に開発を進めることができます。

f:id:aptpod_tech-writer:20210423230452p:plain
Visual Parts SDK の構成図

詳細についてはビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」を参照してください。

ローカル開発環境でサンプルのビジュアルパーツを表示する

@aptpod/data-viz-create-visual-parts-react のパッケージスクリプトを実行して開発用のワークスペースを作成します。

$ npx @aptpod/data-viz-create-visual-parts-react -o <workspace-directory>

作成したワークスペースにはサンプルとしていくつかのビジュアルパーツが付属しています。

f:id:aptpod_tech-writer:20210423062011p:plain
ビジュアルパーツのサンプル

以下の動画では、弊社のモバイル計測アプリ intdash Motion の加速度センサの値を、ワークスペースに含まれるサンプルビジュアルパーツを使って可視化しています。

www.youtube.com

動画で確認できる情報は下記のとおりです。

  1. ワークスペースを作る
  2. 依存パッケージをインストールする
  3. ローカル開発サーバーを起動する
  4. Data Visualizer にローカル開発サーバーのURLを設定する
  5. Sensor Value のサンプルビジュアルパーツを表示する
  6. Motion の計測を開始して、Sensor Value のビジュアルパーツでセンサーの値を可視化する
  7. Horizontal Barsのビジュアルパーツで複数のセンサーの値を可視化する
  8. ビジュアルパーツのLine Graph も同時に表示する
  9. ストアした計測したデータを表示する

Visual Parts SDK で利用可能なデータ

Visual Parts SDK を使用して下記データの連携が可能になります。

Data Visualizer から ビジュアルパーツへは 可視化に必要な情報、ビジュアルパーツから Data Visualizer には、変更されたビジュアルパーツの設定情報を連携することが可能です。

f:id:aptpod_tech-writer:20210423105336p:plain
Visual Parts SDK で連携する情報

Visual Parts SDK を使用した標準ビジュアルパーツ

Data Visualizer に含まれている標準ビジュアルパーツも Visual Parts SDK を使用して作成されています。 Visual Parts SDKを使用してどんなパーツが作成できるのかを知っていただくために、標準ビジュアルパーツの中でもよく使われているものをいくつか紹介します。

単一データ

数値・テキストの表示、またはメーターで表現します。 まずは数値で可視化したい、または最大値に対してどれぐらい近づいたかなどを確認するときに使用します。

f:id:aptpod_tech-writer:20210423053229p:plain
Current Value

f:id:aptpod_tech-writer:20210423053253p:plain
Arc Subdivision Meter

時系列データ

時系列のデータを折れ線グラフで表示したり2つのデータから散布図を可視化します。表示している時間範囲で計測データの前後関係(傾向)や、特徴点、または突出したデータが含まれていないか確認するときに使用します。

f:id:aptpod_tech-writer:20210423112743p:plain
Line Graph

f:id:aptpod_tech-writer:20210423112754p:plain
Scatter

メディアデータ

H.264形式で計測した動画の可視化、またはPCMで計測した音声を再生することができます。 *1

Audio Player には、スペクトログラムを表示する機能も含まれています。

f:id:aptpod_tech-writer:20210423112725p:plain
Video Player

f:id:aptpod_tech-writer:20210423113012p:plain
Audio Player

複数のビジュアルパーツを組み合わせる

ユーザー様自身やパートナー企業様の手で標準ビジュアルパーツを組み合わせてダッシュボードを作成することが可能です。

Visual Parts SDK を使用して作成したパーツは、その他の標準パーツと一緒にダッシュボード上に表示することができます。(下図赤枠)

f:id:aptpod_tech-writer:20210423114606p:plain
複数のビジュアルパーツの表示

ローカル開発環境でフリートマップを作る

ここから本題の作成したフリートマップについて紹介します。

作成したビジュアルパーツの紹介

まずは作成したパーツの紹介をします。 複数の車両の位置と走行速度、SOC (バッテリーの充電率)をリアルタイムで監視し、必要に応じて特定の車両(ドライバー)にアクションを指示するユースケースをイメージして作成しました。

また、マップ表示は、OpenStreetMap を使用してライセンスの範囲内でお手軽に実装できました。

f:id:aptpod_tech-writer:20210423155647p:plain
フリートマップ (完成図)

LIVE計測中の動画はこちらです。

youtu.be

後から走行データ、SOCの残量を確認することも可能です。

youtu.be

Panel Option から OpenStreetMap の表示スタイルも変更できるように実装しました。

youtu.be

ビジュアルパーツを実装する

では、実際にビジュアルパーツの実装の内容を紹介したいと思います。

Visual Parts SDK のワークスペース作成ツール @aptpod/data-viz-create-visual-parts-react を使用します。

ビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」の手順のとおり、事前にサーバーの設定を完了しておきます。

ワークスペースを準備する

ビジュアルパーツを開発するためのワークスペースを作成します。

# ワークスペースのディレクトリを作成します
$ npx @aptpod/data-viz-create-visual-parts-react -o visual-parts-fleet-map

# ワークスペースに移動します
$ cd visual-parts-fleet-map

# 依存パッケージをインストールします
$ npm ci

開発用のディレクトリを作成する

フリートマップの開発で使用するディレクトリ、ファイルを追加します。
追加作成したファイル構成は以下のとおりです。
ソースコードは GitHub を参照してください。

src
  - assets
    - images
     - fleet-map
       - th-fleet-map@3x.png  ....... サムネイル画像です
  - entrypoint
    - fleet-map
      - parts ....................... View のReactサブコンポーネントを実装します
        - bar-sub-division-meter
        - car-maker
        - edge-card
        - edges-panel-title
        - map-zoom-controller
        - scrollbar
      - selector
        - props-selector.ts ........ Visual Parts SDK のデータから View の Component Props に変換します
      - component.tsx .............. View のReactコンポネントを実装します
      - container.tsx .............. Visual Parts SDK と Fleet Map Component を連結します
      - index.ts ................... ビジュアルパーツ Fleet Map を定義します
      - extension.tsx .............. ビジュアルパーツの設定、及びPanel Option を定義します
      - utils.ts ................... ヘルパーを実装します
    - index.ts ..................... エントリポイントの定義します (複数のビジュアルパーツを実装する場合はここにimportの行を追加します)

View を実装する

フリートマップ で表現したいComponent の Props を src/entrypoint/fleet-map/component.tsx に定義します。 この定義に沿って React の Component を実装します。

type Props = {
  size: {
    /** unit: px */
    width: number
    /** unit: px */
    height: number
  }
  openStreetMap: {
    url: string
    attribution: string
  }
  datasets: {
    edgeUUID: string
    edgeName: string
    /** unit:degree */
    lat: number
    /** unit:degree */
    lng: number
    /** unit:degree  */
    heading: number
    /** unit: ratio */
    soc: number
    /** unit: Km/h */
    speed: number
  }[]
}

Map の表示は OpenStreetMap を使用しています。
Leaflet 、及び React Leaflet *2 を使用してお手軽に実現しています。

import React, { memo, useMemo, useState, useCallback, useEffect } from 'react'
import { renderToString } from 'react-dom/server'

import { TileLayer, Marker, Map } from 'react-leaflet'
import { Icon, Point } from 'leaflet'

import { MapZoomController } from './parts/map-zoom-controller'
import { CarMarker } from './parts/car-maker'
import { Scrollbar } from './parts/scrollbar'
import { EdgesPanelTitle } from './parts/edges-panel-title'
import { EdgeCard } from './parts/edge-card'

import * as C from './constant'
import * as S from './style'
import * as utils from './utils'

type Props = {
  ...
}

export const FleetMap: React.VFC<Props> = memo((props) => {
  const { size, openStreetMap, datasets } = props
  const { width, height } = size

  const [centerCoord, setCenterCoord] = useState(
    C.OPEN_STREET_MAP_LAT_LNG_DEFAULT,
  )
  const [zoom, setZoom] = useState(C.OPEN_STREET_MAP_ZOOM_DEFAULT)
  const [selectedEdgeUUID, setSelectedEdgeUUID] = useState(C.EDGE_UNSELECTED)

  // Zoom In / Outイベントハンドラ
  const onZoomIn = useCallback(() => {
    setZoom((prev) => Math.min(prev + 1, C.OPEN_STREET_MAP_ZOOM_IN_LIMIT))
  }, [])
  const onZoomOut = useCallback(() => {
    setZoom((prev) => Math.max(prev - 1, C.OPEN_STREET_MAP_ZOOM_OUT_LIMIT))
  }, [])

  // Mapを表示するサイズ、または OpenSteetMapのURLが変更になったら
  // Mapを再描画するようにKeyも変更する
  const mapKey = `${width}-${height}-${openStreetMap.url}`

  // マップのコンテナスタイル
  const mapContainerStyle = useMemo(
    () => ({
      width,
      height: height - C.LAYOUT_HEADER_HEIGHT,
    }),
    [width, height],
  )

  // マップの Center 座標を更新する
  // Edgeが未選択、または Lat, Lng が無効値なら更新しない
  useEffect(() => {
    datasets.forEach(({ edgeUUID, lat, lng }) => {
      if (edgeUUID === selectedEdgeUUID && utils.isFiniteLatLng(lat, lng)) {
        setCenterCoord([lat, lng])
      }
    })
  }, [datasets, selectedEdgeUUID])

  // Edgeを選択するイベントハンドラを作成する
  // すでに同一のEdgeが選択済みの場合は選択を解除する
  const makeOnSelectEdgeUUID = useCallback((edgeUUID: string) => {
    return () => {
      setSelectedEdgeUUID((prev) => {
        return prev === edgeUUID ? C.EDGE_UNSELECTED : edgeUUID
      })
    }
  }, [])

  //Car Marker の Icon を作成する
  const makeCarMarkerIcon = useCallback(
    (selected: boolean, heading: number) => {
      return new Icon({
        iconUrl: utils.toDataURISchemaSvg(
          renderToString(
            <CarMarker
              size={C.CAR_MARKER_SIZE}
              selected={selected}
              rotationAngle={
                isFinite(heading) ? heading : C.CAR_MARKER_HEADING_DEFAULT
              }
            />,
          ),
        ),
        iconAnchor: [C.CAR_MARKER_SIZE / 2, C.CAR_MARKER_SIZE / 2],
        iconSize: new Point(C.CAR_MARKER_SIZE, C.CAR_MARKER_SIZE),
      })
    },
    [],
  )

  // Edgeの数を判定する
  const numOfEdge = datasets.length
  const numOfActiveEdge = useMemo(
    () =>
      datasets.filter(({ lat, lng }) => utils.isFiniteLatLng(lat, lng)).length,
    [datasets],
  )

  // Action ボタンを押したらDriverに指示を送ろう!!
  // eslint-disable-next-line no-alert
  const doAction = useCallback(() => alert(C.DO_ACTION_MESSAGE), [])

  return (
    <S.Section marginTop={C.LAYOUT_HEADER_HEIGHT}>
      <link
        rel="stylesheet"
        href="https://unpkg.com/leaflet@1.6.0/dist/leaflet.css"
        integrity="sha512-puBpdR0798OZvTTbP4A8Ix/l+A4dHDD0DGqYW6RQ+9jxkRFclaxxQb/SJAWZfWAkuyeQUytO7+7N4QKrDh+drA=='crossorigin='"
      />

      <S.EdgesPanelArea>
        <Scrollbar>
          <S.EdgesPanelBg>
            <S.EdgesPanelHeaderArea>
              <EdgesPanelTitle
                numOfActiveEdge={numOfActiveEdge}
                numOfTotalEdge={numOfEdge}
              />
            </S.EdgesPanelHeaderArea>

            <S.EdgeCardsArea>
              {useMemo(() => {
                return datasets.map((dataset, idx) => (
                  <EdgeCard
                    key={idx}
                    selected={selectedEdgeUUID === dataset.edgeUUID}
                    name={utils.formatEdgeName(dataset.edgeName, C.NO_NAME)}
                    soc={utils.formatSocString(dataset.soc, C.INVALID_STRING)}
                    socRatio={utils.formatSocRatio(
                      dataset.soc,
                      C.SOC_RATIO_DEFAULT,
                    )}
                    speed={utils.formatSppedString(
                      dataset.speed,
                      C.INVALID_STRING,
                    )}
                    driver={C.DRIVER_NAME}
                    onCardClick={makeOnSelectEdgeUUID(dataset.edgeUUID)}
                    onActionClick={doAction}
                  />
                ))
              }, [datasets, doAction, makeOnSelectEdgeUUID, selectedEdgeUUID])}
            </S.EdgeCardsArea>
          </S.EdgesPanelBg>
        </Scrollbar>
      </S.EdgesPanelArea>

      <S.MapArea>
        <Map
          key={mapKey}
          center={centerCoord}
          zoom={zoom}
          zoomControl={C.OPEN_STREET_MAP_ZOOM_CONTROLS_DEFAULT}
          style={mapContainerStyle}
        >
          <TileLayer
            url={openStreetMap.url}
            attribution={openStreetMap.attribution}
          />

          {useMemo(() => {
            return datasets.map((dataset, idx) => (
              <React.Fragment key={idx}>
                {utils.isFiniteLatLng(dataset.lat, dataset.lng) && (
                  <Marker
                    position={[dataset.lat, dataset.lng]}
                    icon={makeCarMarkerIcon(
                      selectedEdgeUUID === dataset.edgeUUID,
                      dataset.heading,
                    )}
                    onClick={makeOnSelectEdgeUUID(dataset.edgeUUID)}
                  />
                )}
              </React.Fragment>
            ))
          }, [
            datasets,
            makeCarMarkerIcon,
            makeOnSelectEdgeUUID,
            selectedEdgeUUID,
          ])}

          <S.MapZoomControllerArea>
            <MapZoomController
              onZoomInClick={onZoomIn}
              onZoomOutClick={onZoomOut}
            />
          </S.MapZoomControllerArea>
        </Map>
      </S.MapArea>
    </S.Section>
  )
})

Container を実装する

Visual Parts SDK から取得したデータを FleetMap Component に連携する処理を実装します。

src/entrypoint/fleet-map/container.tsx

import React, { memo, useEffect, useState } from 'react'
import {
  ExposerEvent,
  ViewBox,
  DataSpecification,
  Value,
} from '@aptpod/data-viz-visual-parts-sdk'

import { FleetMap } from './component'
import {
  useSelectSize,
  useSelectOpenstreetMap,
  useSelectDatasets,
} from './selector/props-selector'
import {
  OPEN_STREET_MAP_URL_DEFAULT,
  OPEN_STREET_MAP_ATTRIBUTION_DEFAULT,
} from './constant'
import { parse as parseExtension, defaultExtension } from './extension'

type Props = {
  comm: ExposerEvent
}

const VIEW_BOX_DEFAULT: ViewBox = { width: 100, height: 100 }
const DATA_SPECS_DEFAULT: DataSpecification[] = []
const VALUES_DEFAULT: Value[] = []

/**
 * data-viz-visual-parts-sdk から取得したデータを FleetMap Component に必要なPropsに連携します。
 */
export const FleetMapContainer: React.FC<Props> = memo((props) => {
  const [viewBox, setViewBox] = useState(VIEW_BOX_DEFAULT)
  const [extension, setExtension] = useState(defaultExtension)
  const [dataSpecifications, setDataSpecifications] = useState(DATA_SPECS_DEFAULT)
  const [values, setValues] = useState(VALUES_DEFAULT)

  useEffect(() => {
    // ビジュアルパーツの表示サイズを取得します。
    props.comm.viewBox.on(setViewBox)

    // ビジュアルパーツの設定情報を取得します。
    props.comm.extension.on((anyExtension: any) => {
      setExtension(parseExtension(anyExtension))
    })

    // Data Visualizer にバインドしているデータの定義リストを取得します。
    props.comm.dataSpecifications.on(setDataSpecifications)

    // Data Visualizer にバインドしているデータに紐づく計測データを取得します。
    props.comm.values.on(setValues)

    // Data Visualizer に、ビジュアルパーツのイベント取得初期設定が完了したことを通知します。
    props.comm.loaded.emit()
  }, [props.comm])

  // Fleet Map Component に指定する Props に変換します。
  const size = useSelectSize({ viewBox })

  const openSteetMap = useSelectOpenstreetMap({
    dataset: {
      url: extension.openStreetMapURL,
      attribution: extension.openStreetMapAttribution,
    },
    default: {
      url: OPEN_STREET_MAP_URL_DEFAULT,
      attribution: OPEN_STREET_MAP_ATTRIBUTION_DEFAULT,
    },
  })

  const datasets = useSelectDatasets({ dataSpecifications, values })

  // View を表示します。
  return (
    <FleetMap size={size} openStreetMap={openSteetMap} datasets={datasets} />
  )
})

src/entrypoint/fleetmap/props-selector.tsx

import { ComponentProps, useMemo } from 'react'
import {
  ViewBox,
  DataSpecification,
  Value,
} from '@aptpod/data-viz-visual-parts-sdk'
import { FleetMap } from '../component'

type FleetMapProps = ComponentProps<typeof FleetMap>

/**
 * FleetMap Component Size Props に変換します。
 */
export const useSelectSize = (params: {
  viewBox: ViewBox
}): FleetMapProps['size'] => {
  const { width, height } = params.viewBox

  const size = useMemo(() => {
    return { width, height }
  }, [width, height])

  return size
}

/**
 * FleetMap Component の OpenStreetMap Props に変換します。
 */
export const useSelectOpenstreetMap = (params: {
  dataset: {
    url: string
    attribution: string
  }
  default: {
    url: string
    attribution: string
  }
}): FleetMapProps['openStreetMap'] => {
  const p = params

  const trimedURL = p.dataset.url.trim()
  const trimedAttribution = p.dataset.attribution.trim()

  const url = trimedURL !== '' ? trimedURL : p.default.url
  const attribution =
    trimedAttribution !== '' ? trimedAttribution : p.default.attribution

  const openStreetMap = useMemo(() => {
    return {
      url,
      attribution,
    }
  }, [url, attribution])

  return openStreetMap
}

/**
 * FleetMap Component の Datasets Props に変換します。
 */
export const useSelectDatasets = (params: {
  dataSpecifications: DataSpecification[]
  values: Value[]
}): FleetMapProps['datasets'] => {
  const { dataSpecifications, values } = params

  const edgeMap: Map<
    string,
    {
      edgeUUID: string
      edgeName: string
      values: number[]
    }
  > = new Map()

  for (const [i, { edgeUUID, edgeName }] of dataSpecifications.entries()) {
    const has = edgeMap.has(edgeUUID)
    if (!has) {
      edgeMap.set(edgeUUID, {
        edgeUUID,
        edgeName,
        values: [],
      })
    }

    const value = values[i]
    if (!value) {
      continue
    }

    const numValue = Number((value.data[value.baseIdx] ?? { v: NaN }).v)

    const edge = edgeMap.get(edgeUUID)
    edge?.values.push(numValue)
  }

  const datasets = [...edgeMap.values()].map((edge) => {
    return {
      edgeUUID: edge.edgeUUID,
      edgeName: edge.edgeName,
      lat: edge.values[0] ?? NaN,
      lng: edge.values[1] ?? NaN,
      heading: edge.values[2] ?? NaN,
      soc: edge.values[3] ?? NaN,
      speed: edge.values[4] ?? NaN,
    }
  })

  return datasets
}

ビジュアルパーツの設定情報を定義する

OpenStreetMap の URL、Attribution を定義します。

src/entrypoint/fleet-map/extension.ts で定義した EXTENSION_CONFIGS は 実装次第では、 Panel Settings の Panel Option からビジュアルパーツの設定を変更することも可能です。 今回は例として、 OpenStreetMap の表示スタイルを変更する設定を実装してみました。

f:id:aptpod_tech-writer:20210423084145p:plain
ビジュアルパーツのPanel Option

src/entrypoint/fleet-map/extension.ts

import * as Z from 'zod'
import { Metadata } from '@aptpod/data-viz-visual-parts-sdk'
import { estimate, estimatePartialObject } from 'src/utils/zod'

/**
 * Extension の型を定義
 */
export type Extension = {
  openStreetMapURL: string
  openStreetMapAttribution: string
}

/**
 * Extension のDefault値を設定します。
 */
export const defaultExtension: Extension = {
  openStreetMapURL: '',
  openStreetMapAttribution: '',
}

/**
 * Extension のスキーマ定義します。
 */
export const schema = {
  openStreetMapURL: Z.string(),
  openSteetMapAttribution: Z.string(),
}

/**
 * Extension の各フィールドをチェックします。
 */
export const parse = (anyExtension: any): Extension => {
  const def = defaultExtension
  const ext = estimatePartialObject<Extension>(anyExtension)

  // eslint-disable-next-line prettier/prettier
  const openStreetMapURL = estimate(schema.openStreetMapURL, ext.openStreetMapURL, def.openStreetMapURL)
  // eslint-disable-next-line prettier/prettier
  const openStreetMapAttribution = estimate(schema.openStreetMapURL, ext.openStreetMapAttribution, def.openStreetMapAttribution)

  return {
    openStreetMapURL,
    openStreetMapAttribution,
  }
}

/**
 * Data VisualizerのPanel Optionに表示する入力項目を定義します。
 */
export const EXTENSION_CONFIGS: Metadata['panelOptionConfig']['extensionConfigs'] = [
  {
    id: 'InputText',
    key: 'openStreetMapURL',
    label: 'OpenStreetMap URL',
    option: { placeholder: 'URL' },
  },
  {
    id: 'InputText',
    key: 'openStreetMapAttribution',
    label: 'OpenStreetMap Attribution',
    option: { placeholder: 'HTML Source Code' },
  },
]

サムネイル画像の準備をする

こちらの手順に沿ってビジュアルパーツにはSVGまたはpng形式のサムネイルを準備します。

フリートマップのサムネイルは、png形式のため Retina 対応の画像も考慮して 幅150x横100 の3倍のpng形式のファイルを使用しました。

f:id:aptpod_tech-writer:20210423070624p:plain
サムネイル画像

index.tsx を作成します。

Data Visualizer がフリートマップを表示するための処理を実装します。

src/entrypoint/fleet-map/index.tsx

import {
  expose,
  Renderer,
  Metadata,
  ExposerEvent,
} from '@aptpod/data-viz-visual-parts-sdk'
import React from 'react'
import { render, unmountComponentAtNode } from 'react-dom'

// Shadow DOM に適用するStyle
import { StyledShadowStyle } from '../../styles/shadow'
// Styled Components を Shadow DOM 以下で適用するための Utility
import { StyleSheetManagerWrapper } from '../../utils/components/style/stylesheet-manager-wrapper'

import { FleetMapContainer } from './container'
import { EXTENSION_CONFIGS, defaultExtension } from './extension'
import thumbnailSrc from 'src/assets/images/fleet-map/th-fleet-map@3x.png'

/**
 * Metadata 作成
 */
const metadata: Metadata = {
  partsType: '@demo/fleet-map',
  partsName: 'Fleet Map',
  groupName: 'Demo',
  panelTagName: 'x-demo-fleet-map',
  getThumbnailURL: (baseURL: string) => `${baseURL}${thumbnailSrc}`,
  panelViewConfig: {
    displayTimestamp: true,
  },
  panelOptionConfig: {
    rangeAtMost: 0,
    canEditColor: false,
    bindDataCountMax: 250,
    extensionConfigs: EXTENSION_CONFIGS,
  },
  defaultExtension,
}

/**
 * Renderer クラスを継承したPluginRendererを定義します。
 */
class PluginRenderer extends Renderer {
  /**
   * 描画を実行します。 1回だけコールします。
   * 状態を変更する場合は、 ExposerEvent のイベントを利用し、 element のDOMを再描画します。
   */
  // eslint-disable-next-line class-methods-use-this
  render(el: HTMLElement, comm: ExposerEvent) {
    // Reactを使用した描画
    render(
      <StyleSheetManagerWrapper>
        <>
          <StyledShadowStyle />
          <FleetMapContainer comm={comm} />
        </>
      </StyleSheetManagerWrapper>,
      el,
    )
  }
  /**
   * element に紐づく子要素のDOMや子要素のイベントハンドラを解放します。
   * HTMLElement は、 render メソッドに引数として渡された HTMLElement (コンテナ)と同じです。
   */
  // eslint-disable-next-line class-methods-use-this
  dispose(el: HTMLElement) {
    unmountComponentAtNode(el)
  }
}

/**
 * 作成した metadata, renderer を公開します。
 */
expose({
  metadata,
  renderer: PluginRenderer,
})

フリートマップ の src/entrypoint/fleet-map/index.tssrc/entrypoint/index.ts に追加します。

src/entrypoint/index.ts

import './fleet-map'

サンプルパーツを削除する

付属のサンプルパーツは不要のため削除します。

  • src/entrypoint/index.ts から、 import './sample/...' の行を削除
  • 付属サンプルのディレクトリを削除
    • src/assets/images/samples
    • src/entrypoint/samples
  • npm uninstall chart.js react-chartjs-2 d3 @types/chart.js @types/d3 binary-search-bound を実行して、サンプルビジュアルパーツが使用していたパッケージをアンインストール

Data Visualizer で表示を確認する

ビジュアルパーツをローカル開発サーバーでホストする

実装したビジュアルパーツをホストするローカル開発サーバーを起動します。

ビジュアルパーツをローカル開発サーバーでホストすることで、ビジュアルパーツの動作確認、または調整など効率的に開発を進めることができます。

$ npm run start
...

--------------------------------

Set the Plugin URL under development to Visual M2M Data Visualizer.
Please set the following URL in Local Plugin URL Settings of Function Menu.

- http://localhost:8080/app.js

--------------------------------

ローカル開発サーバーのURLを入力する

ローカル開発サーバー起動時に表示されたURL (http://localhost:8080/app.js) を Data Visualizer の Visual Parts Plugin Settings Plg の設定画面で保存します。

Plg のメニューを表示するためには、ビジュアルパーツの作成ガイド「Visual Parts SDKによるData Visualizer用ビジュアルパーツの作成」 を参照してください。

f:id:aptpod_tech-writer:20210423080316p:plain
ローカルサーバーのURLを設定する

ビジュアルパーツを選択する

Data Visualizer で開発した Fleet Map のビジュアルパーツの選択が可能になっていることを確認します。 今回作成したパーツは Demo グループに表示されます。( index.tsx の groupName で変更することができます)

f:id:aptpod_tech-writer:20210423080855p:plain
Fleet Map のビジュアルパーツ選択

デモ用計測データの定義を準備する

デモ用の計測データを表示するための定義ファイル を Data Settings にインポートします。(Data Visualizer の使用方法については、マニュアルをご覧ください

f:id:aptpod_tech-writer:20210423080948p:plain
Fleet Map で使用する定義データ

データをバインドする

作成した Fleet Map の ビジュアルパーツにデータを一つずつ追加します。

f:id:aptpod_tech-writer:20210423182344p:plain
Bind Data

計測データを確認する

最後にデモの計測を開始し、Data Visualizer を LIVE再生すると表示ができました。

f:id:aptpod_tech-writer:20210423160025p:plain
完成後のビジュアルパーツ

まとめ

Visual Parts SDK を使用することで、ユーザー様ご自身で Data Visualizer をカスタマイズできることを実感いただけましたでしょうか?少しでも実際に近い開発や運用のイメージを掴んで頂くため、今回はフリートマップをサンプルとして選んでみました。

読者の皆様に本製品を活用いただくことで、DX表現のパートナーとしてお手伝いできることを楽しみにしております。 また、Data Visualizer ビジュアルパーツをカスタマイズ開発するための一助になれば幸いです。

以上です。ありがとうございました!

*1:別途 intdash サーバーからストリーミングでメディアデータを受け取る、またはストアしているメディアデータにアクセスするAPIが必要になります。当APIを使用するためのSDKは開発中です。

*2:React Leafletのv3.0.0以降はライセンスがHippocratic Licenseのため、v2.8.0を使用しています