反応ネイティブで地図クラスタリングをする方法


この記事では、クラスタリングを使用してマップ上に多数のマーカーを表示する方法を説明し、その実装に必要な反応するネイティブコードを提供します.
ほとんどの開発者は、ネイティブまたは他のプログラミング言語のモバイルアプリケーションの開発を遅かれ早かれ以降の作業は、アプリにマップを追加する必要があるプロジェクトを取得します.ここでは、彼らはパフォーマンスの不足の挑戦に会います.より多くのデータは、より悪いパフォーマンスになることができます.これは過度の再レンダリングですそれは、それが時間がかかることより、コンポーネントのより頻繁にレンダリングを行うことを意味します.そのようなレンダリングは、コンポーネント(この場合のマーカー)に関係していないが、状態の一部の変化に関係しない.マーカーの状態の変更を伴う必要な再レンダリングだけを許している間、パフォーマンスは明らかによりよくなる.
また、マーカーの多くで操作すると、マップが過充電となり、負のモバイルUXに影響を与えることができます.一度にすべてのマーカーを表示すると、貧しいユーザー体験を作成します.Upsilonitでは、我々は最近のプロジェクトの1つで我々の地図のイベントでこの問題を抱えました.ピンの数が少ないので、地図はすばらしく見えた.しかし、マーカーの数を増やすと、マップが乱雑に見え、イベントが起こっているかの最高の概要を提供していません.
ここで最も直感的な解決策はクラスタリングを使用することです.一緒にマーカーをクラスタリング(ズームアウト機能)とデクラスタリング(ズーム機能)では、大幅にアプリケーションのパフォーマンスを向上させることができますが、より接近的な方法でデータを提示する.本稿では、マップクラスタリングをどのように行うかを詳細に説明し、使用するプログラミングコードを提供します.

セットアップ

  • インストールNode.js
  • インストールExpo CLI and yarn . npm install expo-cli yarn --global
  • あなたのエキスポプロジェクトを作成します.expo init rn-clustering-example
  • プロジェクトに移動します.cd rn-clustering-example
  • インストールSuper Cluster その他の依存関係.yarn add react-native-maps supercluster @mapbox/geo-viewport
  • サーバを起動します.expo start
  • 実装


    基本的なマップの作成から始めましょうreact-native-maps ライブラリといくつかのマーカーを追加します.これは、画面上のマップコンポーネントをレンダリングするために使用されるネイティブのMapViewを構築します.MapViewの中にマーカーコンポーネントを渡し、地図上に表示します.
    import React, { useEffect, useState } from 'react'
    import { Dimensions, StyleSheet, View } from 'react-native'
    import MapView, { Marker, Region } from 'react-native-maps'
    
    import MapZoomPanel from './components/MapZoomPanel'
    
    function getRandomLatitude(min = 48, max = 56) {
      return Math.random() * (max - min) + min
    }
    
    function getRandomLongitude(min = 14, max = 24) {
      return Math.random() * (max - min) + min
    }
    interface Markers {
      id: number
      latitude: number
      longitude: number
    }
    
    function App(): JSX.Element {
      const [zoom, setZoom] = useState(18)
      const [markers, setMarkers] = useState<Markers[]>([
        { id: 0, latitude: 53.91326738786109, longitude: 27.523712915343737 },
      ])
      const region: Region = {
        latitude: 53.91326738786109,
        longitude: 27.523712915343737,
        latitudeDelta: 0.1,
        longitudeDelta: 0.1,
      }
      const map = React.useRef(null)
    
      const generateMarkers = React.useCallback((lat: number, long: number) => {
        const markersArray = []
    
        for (let i = 0; i < 50; i++) {
          markersArray.push({
            id: i,
            latitude: getRandomLatitude(lat - 0.05, lat + 0.05),
            longitude: getRandomLongitude(long - 0.05, long + 0.05),
          })
        }
        setMarkers(markersArray)
      }, [])
    
      useEffect(() => {
        generateMarkers(region.latitude, region.longitude)
      }, [])
    
      const getRegionForZoom = (lat: number, lon: number, zoom: number) => {
        const distanceDelta = Math.exp(Math.log(360) - zoom * Math.LN2)
        const { width, height } = Dimensions.get('window')
        const aspectRatio = width / height
        return {
          latitude: lat,
          longitude: lon,
          latitudeDelta: distanceDelta * aspectRatio,
          longitudeDelta: distanceDelta,
        }
      }
    
      const mapZoomIn = () => {
        if (zoom > 18) {
          setZoom(18)
        } else {
          setZoom(zoom + 1)
          const regn = getRegionForZoom(region.latitude, region.longitude, zoom + 1)
          map.current.animateToRegion(regn, 200)
        }
      }
    
      const mapZoomOut = () => {
        if (zoom < 3) {
          setZoom(3)
        } else {
          setZoom(zoom - 1)
          const regn = getRegionForZoom(region.latitude, region.longitude, zoom - 1)
          map.current.animateToRegion(regn, 200)
        }
      }
    
      return (
        <View style={styles.container}>
          <MapView ref={map} mapType="hybrid" style={styles.mapView} initialRegion={region}>
            {markers.map((item) => (
              <Marker
                key={item.id}
                coordinate={{
                  latitude: item.latitude,
                  longitude: item.longitude,
                }}></Marker>
            ))}
          </MapView>
          <MapZoomPanel
            onZoomIn={() => {
              mapZoomIn()
            }}
            onZoomOut={() => {
              mapZoomOut()
            }}
          />
        </View>
      )
    }
    
    const styles = StyleSheet.create({
      container: {
        flex: 1,
        backgroundColor: '#fff',
        alignItems: 'center',
        justifyContent: 'center',
      },
      mapView: { flex: 1, width: '100%', height: '100%' },
    })
    
    export default App
    
    今まで使ったことがあるならreact-native-community ’s react-native-maps あなたの反応ネイティブアプリにネイティブのMapViewを追加するパッケージは、おそらくピン、アイコン、および他のコンテンツをマップの混乱の共通の問題に遭遇した.

    我々はこの問題を解決することを決めたmapbox/supercluster 図書館.
    スーパークラスタは混乱のための地理空間点クラスタリング解であるreact-native-maps mapviewこれは、簡単かつ迅速に実装することですし、それはかなりの箱のうち動作します.
    クラスタロジックは、マップをズームアウトするときにマーカーのいくつかの量を組み合わせて(5 +のために言う)1つのクラスタ化マーカーに使用されます.クラスターマーカーの中にマーカーの数が含まれており、我々はそれを押すと、それは地図をズームし、マーカーマーカーの内部にあったマーカーを示しています.
    我々はまたmapbox/geo-viewport ライブラリを中心に地図上の境界ボックス&ズームコンボをオンにします.
    を用いたクラスタリングの実装SuperCluster MapViewとの統合は、いくつかの手順で行うことができます.
  • インストールSuperCluster and mapbox/geo-viewport 糸の使用yarn add supercluster @mapbox/geo-viewport ) またはNPMnpm install --save supercluster @mapbox/geo-viewport )
  • クラスタリングを統合するreact-native-maps’ MapViewは、我々はカスタムマーカー、種類、および地理データ処理、スタイリングクラスタなどに使用されるヘルパーが必要ですGithub .
  • それを終了するには、基本マップ、カスタムコンポーネント、およびクラスタロジックを一緒に配置する必要があります.
  • import React, { forwardRef, memo, useEffect, useMemo, useRef, useState } from 'react'
    import { Dimensions, LayoutAnimation, Platform } from 'react-native'
    import MapView, { MapViewProps, Polyline } from 'react-native-maps'
    import SuperCluster from 'supercluster'
    
    import { MapClusteringProps } from './ClusteredMapViewTypes'
    import ClusterMarker from './ClusteredMarker'
    import {
      calculateBBox,
      generateSpiral,
      isMarker,
      markerToGeoJSONFeature,
      returnMapZoom,
    } from './helpers'
    
    const ClusteredMapView = forwardRef<MapClusteringProps & MapViewProps, any>(
      (
        {
          radius,
          maxZoom,
          minZoom,
          minPoints,
          extent,
          nodeSize,
          children,
          onClusterPress,
          onRegionChangeComplete,
          onMarkersChange,
          preserveClusterPressBehavior,
          clusteringEnabled,
          clusterColor,
          clusterTextColor,
          clusterFontFamily,
          spiderLineColor,
          layoutAnimationConf,
          animationEnabled,
          renderCluster,
          tracksViewChanges,
          spiralEnabled,
          superClusterRef,
          ...restProps
        },
        ref,
      ) => {
        const [markers, updateMarkers] = useState([])
        const [spiderMarkers, updateSpiderMarker] = useState([])
        const [otherChildren, updateChildren] = useState([])
        const [superCluster, setSuperCluster] = useState(null)
        const [currentRegion, updateRegion] = useState(restProps.region || restProps.initialRegion)
    
        const [isSpiderfier, updateSpiderfier] = useState(false)
        const [clusterChildren, updateClusterChildren] = useState(null)
        const mapRef = useRef()
    
        const propsChildren = useMemo(() => React.Children.toArray(children), [children])
    
        useEffect(() => {
          const rawData = []
          const otherChildren = []
    
          if (!clusteringEnabled) {
            updateSpiderMarker([])
            updateMarkers([])
            updateChildren(propsChildren)
            setSuperCluster(null)
            return
          }
    
          propsChildren.forEach((child, index) => {
            if (isMarker(child)) {
              rawData.push(markerToGeoJSONFeature(child, index))
            } else {
              otherChildren.push(child)
            }
          })
    
          const superCluster = new SuperCluster({
            radius,
            maxZoom,
            minZoom,
            minPoints,
            extent,
            nodeSize,
          })
    
          superCluster.load(rawData)
    
          const bBox = calculateBBox(currentRegion)
          const zoom = returnMapZoom(currentRegion, bBox, minZoom)
          const markers = superCluster.getClusters(bBox, zoom)
    
          updateMarkers(markers)
          updateChildren(otherChildren)
          setSuperCluster(superCluster)
    
          superClusterRef.current = superCluster
        }, [propsChildren, clusteringEnabled])
    
        useEffect(() => {
          if (!spiralEnabled) {
            return
          }
    
          if (isSpiderfier && markers.length > 0) {
            const allSpiderMarkers = []
            let spiralChildren = []
            markers.map((marker, i) => {
              if (marker.properties.cluster) {
                spiralChildren = superCluster.getLeaves(marker.properties.cluster_id, Infinity)
              }
              const positions = generateSpiral(marker, spiralChildren, markers, i)
              allSpiderMarkers.push(...positions)
            })
    
            updateSpiderMarker(allSpiderMarkers)
          } else {
            updateSpiderMarker([])
          }
        }, [isSpiderfier, markers])
    
        const _onRegionChangeComplete = (region) => {
          if (superCluster && region) {
            const bBox = calculateBBox(region)
            const zoom = returnMapZoom(region, bBox, minZoom)
            const markers = superCluster.getClusters(bBox, zoom)
            if (animationEnabled && Platform.OS === 'ios') {
              LayoutAnimation.configureNext(layoutAnimationConf)
            }
            if (zoom >= 18 && markers.length > 0 && clusterChildren) {
              if (spiralEnabled) {
                updateSpiderfier(true)
              }
            } else {
              if (spiralEnabled) {
                updateSpiderfier(false)
              }
            }
            updateMarkers(markers)
            onMarkersChange(markers)
            onRegionChangeComplete(region, markers)
            updateRegion(region)
          } else {
            onRegionChangeComplete(region)
          }
        }
    
        const _onClusterPress = (cluster) => () => {
          const children = superCluster.getLeaves(cluster.id, Infinity)
          updateClusterChildren(children)
    
          if (preserveClusterPressBehavior) {
            onClusterPress(cluster, children)
            return
          }
    
          const coordinates = children.map(({ geometry }) => ({
            latitude: geometry.coordinates[1],
            longitude: geometry.coordinates[0],
          }))
    
          mapRef.current.fitToCoordinates(coordinates, {
            edgePadding: restProps.edgePadding,
          })
    
          onClusterPress(cluster, children)
        }
    
        return (
          <MapView
            {...restProps}
            ref={(map) => {
              mapRef.current = map
              if (ref) {
                ref.current = map
              }
              restProps.mapRef(map)
            }}
            onRegionChangeComplete={_onRegionChangeComplete}>
            {markers.map((marker) =>
              marker.properties.point_count === 0 ? (
                propsChildren[marker.properties.index]
              ) : !isSpiderfier ? (
                renderCluster ? (
                  renderCluster({
                    onPress: _onClusterPress(marker),
                    clusterColor,
                    clusterTextColor,
                    clusterFontFamily,
                    ...marker,
                  })
                ) : (
                  <ClusterMarker
                    key={`cluster-${marker.id}`}
                    {...marker}
                    onPress={_onClusterPress(marker)}
                    clusterColor={
                      restProps.selectedClusterId === marker.id
                        ? restProps.selectedClusterColor
                        : clusterColor
                    }
                    clusterTextColor={clusterTextColor}
                    clusterFontFamily={clusterFontFamily}
                    tracksViewChanges={tracksViewChanges}
                  />
                )
              ) : null,
            )}
            {otherChildren}
            {spiderMarkers.map((marker) => {
              return propsChildren[marker.index]
                ? React.cloneElement(propsChildren[marker.index], {
                    coordinate: { ...marker },
                  })
                : null
            })}
            {spiderMarkers.map((marker, index) => (
              <Polyline
                key={index}
                coordinates={[marker.centerPoint, marker, marker.centerPoint]}
                strokeColor={spiderLineColor}
                strokeWidth={1}
              />
            ))}
          </MapView>
        )
      },
    )
    
    ClusteredMapView.defaultProps = {
      clusteringEnabled: true,
      spiralEnabled: true,
      animationEnabled: true,
      preserveClusterPressBehavior: false,
      layoutAnimationConf: LayoutAnimation.Presets.spring,
      tracksViewChanges: false,
      // SuperCluster parameters
      radius: Dimensions.get('window').width * 0.06,
      maxZoom: 20,
      minZoom: 1,
      minPoints: 2,
      extent: 512,
      nodeSize: 64,
      // Map parameters
      edgePadding: { top: 50, left: 50, right: 50, bottom: 50 },
      // Cluster styles
      clusterColor: '#00B386',
      clusterTextColor: '#FFFFFF',
      spiderLineColor: '#FF0000',
      // Callbacks
      onRegionChangeComplete: () => {},
      onClusterPress: () => {},
      onMarkersChange: () => {},
      superClusterRef: {},
      mapRef: () => {},
    }
    
    export default memo(ClusteredMapView)
    
    上記の手順を実行した後、クラスタリング機能を持つカスタムMapViewコンポーネントを取得します.
    カスタムマップがどのように見えますか

    私たちはrepository 私たちのGithubでは、より詳細にコードの例を探ることができます.また、ガイドラインとしてこの記事を使用することを決定する場合は、すべての作業を行うためにいくつかの余分なコンポーネントが必要な場合がありますので、我々が使用したプログラミングプログラムのコードを使用してgithubをチェックアウトしてください.

    思考の結論


    クラスタリングは、地図の光になり、ユーザーが代わりにマーキングマーカーの上に地図を非表示に起こっているすべての明確な概要を持つことができます.すぐにポイントの小さなグループに集約あなたのポイントを参照してください.これは、どのように多くのポイントが領域の範囲内で存在するかについてより良い理解を提供する.それはあなたのマップ上で作業中にパフォーマンスとUXの両方を考えることが重要である理由です.マップのクラスタリングを使用すると、優れたユーザー体験を提供することができます高速、より応答性のアプリケーションを構築することができます.
    質問がある?我々は、任意のフィードバックを歓迎し、コメントのセクションでディスカッションを開始してうれしいです.