aptpod Advent Calendar 2022 6æ¥ç®ã®èšäºãæ
åœãããVisual M2M ã°ã«ãŒãã®çœéã§ãã åŒç€Ÿè£œåã® Visual M2M Data Visualizer ã§ã¯ãèšæž¬ããŒã¿ãå¯èŠåããããã®æ§ã
ãªããžã¥ã¢ã«ããŒããæäŸããŠããŸãã ãã®äžã®äžã€ã«ãèšæž¬ããŒã¿ã«å«ãŸããäœçœ®æ
å ±ãããšã«Googleãããã«çŸåšäœçœ®ã衚瀺ããããžã¥ã¢ã«ããŒããå«ãŸããŠããŸãã ä»åã¯ãäžå³ã®ããã« Googleãããã®ããžã¥ã¢ã«ããŒãã3Dã§è¡šçŸããæ©èœã詊ããŠã¿ãã®ã§ç޹ä»ããããšæããŸãã ããžã¥ã¢ã«ããŒãã§ Googleãããã3Dã§è¡šç€º ã¯ããã« ãããIDãæºåãã å®è£
ãã Data Visualizer ã®èšæž¬ããŒã¿ã䜿çšããŠå¯èŠåãã ãããã« ã¯ããã« åŒç€Ÿè£œåã® Visual M2M Data Visualizer 㯠Google Chrome ã®Webãã©ãŠã¶ã§æäŸããŠãããGoogleãããã®ããžã¥ã¢ã«ããŒãã¯ã Google Maps Platform ããæäŸãããŠãã JavasScript API ã䜿çšããŠããŸãã ãã®Googleãããã3Dã§è¡šçŸããããã« WebGL Overlay View ã§æ§ç¯ãã 3D ããã ãšã¯ã¹ããªãšã³ã¹ ãåèã«è©ŠããŠã¿ãŸããã ãããIDãæºåãã Google Maps Platform ã§ã¯ãGoogle Cloud Console ã§äœæãããããã®ã¹ã¿ã€ã«ã ãããID ã«é¢é£ã¥ããããšãã§ããŸãã JavaScript API ã§ WebGL ã®æ©èœãå©çšããããã«ã¯ããã¯ã¿ãŒå°å³ãæå¹ã«ãããããIDãå¿
èŠã«ãªããŸãã èšå®æ¹æ³ã«ã€ããŠã¯äžèšãªã³ã¯å
ã®ããŒãžãåç
§ãã ããã developers.google.com å®è£
ãã 现ããªå®è£
æ¹æ³ã¯ãåAPIã®èª¬æã«ã€ããŠã¯äžèšãªã³ã¯å
ã®ã¹ããã4ã8ã§èª¬æãæ²èŒãããŠããŸãã developers.google.com ããã§ã¯äžèš2ç¹ã衚瀺ããããã®å®è£
ã玹ä»ããŸãã 3Dã¢ãã«ã§çŸåšäœçœ®ã®è¡šç€ºãã GeoJSON ã䜿çšããŠè»è·¡ã衚瀺ãã äžèšNPMããã±ãŒãžã䜿çšããŠãTypeScriptãReact ã§å®è£
ããŸãã typescript react react-dom google-map-react three @types/google-map-react @types/google.maps npm i -S typescript react react-dom google-map-react three npm i -D @types/google-map-react @types/google.maps React ã® Component ãå®è£
ããŸãã 3Dã¢ãã«ã®GLTFããŒã¿ã¯ ãã¡ãã®ãµã³ãã«ããŒã¿ ãæåããŸããã import React , { memo , useEffect , useCallback , useRef , useState } from 'react' import GoogleMapReact from 'google-map-react' import * as THREE from 'three' import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js' // WebGLOverlayView ã®ãµã³ãã«ã§å
¬éãããŠããGLTFããŒã¿ãåç
§ããŸãã import PIN_GLTF from './pin.gltf' // åœã³ã³ããŒãã³ãã§æå®ããäœçœ®æ
å ±ã®åã§ãã type Coordinate = { lat: number lng: number heading: number } // åœã³ã³ããŒãã³ãã®Propsã®åã§ãã type Props = { /** Googleãããã衚瀺ããããã® Api Key */ mapApiKey: string /** ãã¯ã¿ãŒå°å³ãæå¹ã«ãããããIDãæå®ããŸãã */ mapId: string /** Googleãããã®åæè¡šç€ºäœçœ®æ
å ± */ defaultCenter: GoogleMapReact.Coords /** Googleãããã®åæè¡šç€ºãºãŒã å€ */ defaultZoom: number /** 3Dã¢ãã«ã®è¡šç€ºäœçœ®æ
å ± */ model3dCoordinate: Coordinate | undefined /** è»è·¡ã®äœçœ®æ
å ±ãªã¹ã */ trajectoryCoordinates: Coordinate [] } const MODEL_3D_ALTITUDE = 80 const TRAJECTORY_STROKE_COLOR = '#a00' const TRAJECTORY_STROKE_WEIGHT = 6 export const GoogleMaps3dSample: React.FC < Props > = memo (( props ) => { const { mapApiKey , mapId , defaultCenter , defaultZoom , model3dCoordinate , trajectoryCoordinates , } = props // ããŒããã Google Maps Api ã®ã€ã³ã¹ã¿ã³ã¹ãæ ŒçŽããŸãã const [ mapApi , setMapApi ] = useState < { map: google.maps. Map maps: typeof google.maps } | null >( null ) // WebGLOverlayView ã§3Dã¢ãã«ã®è¡šç€ºäœçœ®ãåç
§ããããã®å€æ°ã«ã³ããŒããŸãã const refModel3dCoordinate = useRef < Coordinate | undefined >() useEffect (() => { refModel3dCoordinate.current = model3dCoordinate } , [ model3dCoordinate ] ) // Googleãããã«è¡šç€ºããè»è·¡ã®ã¹ã¿ã€ã«ãå®çŸ©ããŸãã useEffect (() => { mapApi?.map.data.setStyle (() => { return { strokeColor: TRAJECTORY_STROKE_COLOR , strokeWeight: TRAJECTORY_STROKE_WEIGHT , } } ) } , [ mapApi ] ) // è»è·¡ã®ããŒã¿ãGeoJSONãã©ãŒãããã«å€æããŠGoogleãããã«è¡šç€ºããŸãã useEffect (() => { if ( ! mapApi ) { return () => {} } const geoJSON = { type : 'FeatureCollection' , features: [ { type : 'Feature' , properties: {} , geometry: { type : 'MultiLineString' , coordinates: [ trajectoryCoordinates.map (( { lat , lng } ) => [ lng , lat ] ), ] , } , } , ] , } const features = mapApi.map.data.addGeoJson ( geoJSON ) return () => { features.forEach (( feature: any ) => { mapApi.map.data.remove ( feature ) } ) } } , [ mapApi , trajectoryCoordinates ] ) // WebGLOverLayView ã䜿çšããŠGoogle Mapã«3Dã¢ãã«ã衚瀺ããŸãã useEffect (() => { if ( ! mapApi ) { return } const webGLOverlayView = new mapApi.maps.WebGLOverlayView () let scene: THREE.Scene let camera: THREE.PerspectiveCamera let renderer: THREE.WebGLRenderer const model3dGroup: THREE.Group = new THREE.Group () webGLOverlayView.onAdd = () => { // SceneãCamera ã®æ
å ±ãã»ããã¢ããããŸãã scene = new THREE.Scene () camera = new THREE.PerspectiveCamera () const ambientLight = new THREE.AmbientLight ( 0xffffff , 0.75 ) scene.add ( ambientLight ) const directionalLight = new THREE.DirectionalLight ( 0xffffff , 0.25 ) directionalLight.position. set( 0.5 , -1 , 0.5 ) scene.add ( directionalLight ) // 3Dã¢ãã«(GLTF)ãããŒãããŸãã const loader = new GLTFLoader () const source = PIN_GLTF loader.load ( source , ( gltf ) => { // ããŒãããGLTFã®ã¹ã±ãŒã«ãå§¿å¢è§ã®è¡šç€ºèª¿æŽ gltf.scene.scale. set( 25 , 25 , 25 ) gltf.scene.rotation.x = ( 180 * Math .PI ) / 180 model3dGroup.add ( gltf.scene ) scene.add ( model3dGroup ) } ) } webGLOverlayView.onContextRestored = ( { gl } ) => { // renderer ãäœæããŸãã renderer = new THREE.WebGLRenderer ( { canvas: gl.canvas , context: gl , ...gl.getContextAttributes (), } ) renderer.autoClear = false } webGLOverlayView.onDraw = ( { transformer } ) => { // 3Dã¢ãã«ã衚瀺ããäœçœ®æ
å ±ãäœæããŸãã const latLngAltitudeLiteral = { lat: refModel3dCoordinate.current?.lat ?? 0 , lng: refModel3dCoordinate.current?.lng ?? 0 , altitude: MODEL_3D_ALTITUDE , } // 3Dã¢ãã«ã®äœçœ®æ
å ±ãç¡å¹ãªå Žåã¯é衚瀺ã«ããŸãã model3dGroup.visible = Boolean ( refModel3dCoordinate.current ) // åç
§ãã3Dã¢ãã«ã®Z軞ã®å転è§åºŠãèšå®ããŸãã // heading ãšå転æ¹åãéã®ãããå転ããŠããŸãã model3dGroup.rotation.z = ( -1 * ( refModel3dCoordinate.current?.heading ?? 0 ) * Math .PI ) / 180 // 3Dã¢ãã«ã®è¡šç€ºæ
å ±ã renderer ã«åæ ããŸãã const matrix = transformer.fromLatLngAltitude ( latLngAltitudeLiteral ) camera.projectionMatrix = new THREE.Matrix4 () .fromArray ( matrix ) webGLOverlayView.requestRedraw () renderer.render ( scene , camera ) renderer.resetState () } webGLOverlayView.setMap ( mapApi.map ) } , [ mapApi ] ) // MouseDown ã®ã€ãã³ããã³ãã©ãç¡å¹ã«ããŸãã // Data Visualizer æ¬äœã§ãã©ãã°ã€ãã³ãã䜿çšããããã§ãã const onMouseDownEventCancel = useCallback ( ( evt: React.MouseEvent < HTMLElement >) => { evt.preventDefault () } , [] , ) return ( < div role = "button" tabIndex = { 0 } style = {{ width: '100%' , height: '100%' }} onMouseDown = { onMouseDownEventCancel } > < GoogleMapReact bootstrapURLKeys = {{ key: mapApiKey , // ãã¯ã¿ãŒå°å³ãæå¹ã«ãããããIDãæå¹ã«ãããããversion ã« beta ãæå®ããŸãã version: 'beta' , }} options = {{ mapId , tilt: 60 , heading: 0 , }} defaultCenter = { defaultCenter } defaultZoom = { defaultZoom } onGoogleApiLoaded = { setMapApi } / > < /div > ) } ) äžèšãœãŒã¹ã³ãŒãã§å®è£
ãã React Component ãåŒã³åºããŸãã mapId ã¯ãäºåã«æºåãããã¯ã¿ãŒå°å³ãæå¹ã«ãããããIDãæå®ããŸãã mapApiKey 㯠Google Maps Platform ã§äœæããAPIããŒãæå®ããŸããAPIããŒã®äœææé ã«ã€ããŠã¯ ãã¡ã ãã確èªãã ããã < GoogleMaps3dSample mapApiKey = "xxxxxx" mapId = "xxxxxx" defaultCenter = {{ lat: 35.68783052263802 , lng: 139.71728196798034 , }} defaultZoom = { 19 } model3dCoordinate = {{ lat: 35.68781633 , lng: 139.7180315 , heading: 80 , }} trajectoryCoordinates = {[ { lat: 35.68778533 , lng: 139.71701067 , heading: 79 } , { lat: 35.68778533 , lng: 139.71782767 , heading: 79 } , { lat: 35.68778533 , lng: 139.71782767 , heading: 80 } , { lat: 35.68778817 , lng: 139.71784633 , heading: 80 } , { lat: 35.68778817 , lng: 139.71784633 , heading: 80 } , { lat: 35.687791 , lng: 139.71786467 , heading: 79 } , { lat: 35.687791 , lng: 139.71786467 , heading: 79 } , { lat: 35.68779417 , lng: 139.717883 , heading: 79 } , { lat: 35.68779417 , lng: 139.717883 , heading: 79 } , { lat: 35.68779767 , lng: 139.71790067 , heading: 79 } , { lat: 35.68779767 , lng: 139.71790067 , heading: 79 } , { lat: 35.687801 , lng: 139.71791883 , heading: 79 } , { lat: 35.687801 , lng: 139.71791883 , heading: 79 } , { lat: 35.68780417 , lng: 139.7179375 , heading: 79 } , { lat: 35.68780417 , lng: 139.7179375 , heading: 79 } , { lat: 35.687807 , lng: 139.71795633 , heading: 79 } , { lat: 35.687807 , lng: 139.71795633 , heading: 79 } , { lat: 35.68780933 , lng: 139.717975 , heading: 79 } , { lat: 35.68780933 , lng: 139.717975 , heading: 79 } , { lat: 35.68781283 , lng: 139.71799333 , heading: 79 } , { lat: 35.68781283 , lng: 139.71799333 , heading: 79 } , { lat: 35.687815 , lng: 139.7180125 , heading: 79 } , { lat: 35.687815 , lng: 139.7180125 , heading: 79 } , { lat: 35.68781633 , lng: 139.7180315 , heading: 80 } , ]} / > 以äžã§äžå³ã®ããã«Googleãããäžã«3Dã¢ãã«ãåã³è»è·¡ã衚瀺ããããšã§ããŸããã å®è¡çµæ01 å®è¡çµæ02 Data Visualizer ã®èšæž¬ããŒã¿ã䜿çšããŠå¯èŠåãã æ¬¡ã«ãVisual M2M Data Visualizer ã®ããžã¥ã¢ã«ããŒãã« Googleããã3Dã®ããžã¥ã¢ã«ããŒãã衚瀺ããŠã¿ãŸããã äœçœ®æ
å ±ãå«ãèµ°è¡ããŒã¿ã¯ãåŒç€Ÿè£œåã® Visual Parts SDK ã䜿çšã㊠Visual M2M Data Visualizer ããååŸããåçæéã«æ²¿ã£ãŠå¯èŠåããŠããŸãã ãŸããä»ã®ããžã¥ã¢ã«ããŒããšã®æ¯èŒãäœçœ®æ
å ±ã確èªãããããOpen Street Mapã緯床ã軜床ã®å€ã衚瀺ããããžã¥ã¢ã«ããŒãã衚瀺ããŸããã youtu.be Visual Parts SDK ãå«ãåŒç€Ÿè£œåã«é¢ãããåãåããã¯äžèšãªã³ã¯å
ãŸã§ãé¡ãããŸãã www.aptpod.co.jp ãããã« æ°ããæ©èœã詊ããŠå®çŸã§ããç¬éã¯ããã€ã«ãªã£ãŠããã³ã·ã§ã³ãäžããŸããã ä»åã¯éè·¯ã«æ¥ããŠããç§»åäœã®å¯èŠåãšãªããŸããããä»åŸã¯é£è¡ããŠããç§»åäœã®å¯èŠåãè»è·¡ã衚çŸããŠã¿ãããšæããŸãã