본문 바로가기
Open Source/google_map_with_direction_indicator

[Flutter] GoogleMap에 방향 지시자 추가하는 패키지 만들기 - 3

by 6cess 2024. 1. 29.

구현 로직 설명 - 위젯 부분

1. 지도 위에 방향 지시자 겹쳐놓기

Stack 위젯 안에 google_maps_flutter에서 제공하는 GoogleMap위젯을 위치시키고 그 위에 방향 지시자 위젯들이 렌더링되도록 한다.

Stack(
  children: [
    GoogleMap(
      ...
    ),
    if (widget.isIndicatorVisible && widget.markers.isNotEmpty)
      ..._indicatorOffsetList.map<Widget>((el) {
        return Positioned(
          ...
        );
      }).toList(),
  ],
);

 

2.  GoogleMap 위젯이 받는 파라미터 대신 받기

패키지에서 제공하는 위젯이 구글맵 위젯의 부모 위젯이 되기 때문에 부모 위젯에서 구글맵 위젯이 받는 파라미터를 대신 받도록 한다.

class GoogleMapWithDirectionIndicator extends StatefulWidget {
  const GoogleMapWithDirectionIndicator({
    super.key,
    required this.initialCameraPosition,
    this.onMapCreated,
    this.gestureRecognizers = const <Factory<OneSequenceGestureRecognizer>>{},
    this.webGestureHandling,
    
    ... // 구글맵 위젯이 받는 파라미터 대신 받아서 전달하기
    
   	// 구글맵 위젯의 파라미터들 외에 추가로 필요한 파라미터
    this.height,
    this.width,
    required this.controller,
    MaterialColor indicatorColor = Colors.blue,
    this.indicatorSize = const Size(30, 30),
    this.isIndicatorVisible = true,
  });

  final MapCreatedCallback? onMapCreated;
  final CameraPosition initialCameraPosition;
  
  ...

  final Completer<GoogleMapController> controller;
  final double? width;
  final double? height;
  final Size indicatorSize;
  final bool isIndicatorVisible;

  @override
  State<GoogleMapWithDirectionIndicator> createState() =>
      _GoogleMapWithDirectionIndicatorState();
}

 

3. 추가로 필요한 파라미터 작성

구글맵 위젯의 크기나 방향 지시자 위젯의 표시 여부, 크기, 색상, 컨트롤러 등이 필요한 파라미터를 추가로 받도록 작성한다.

this.height,
this.width,
required this.controller,
this.indicatorColor = Colors.blue,
this.indicatorSize = const Size(30, 30),
this.isIndicatorVisible = true,

final double? width;
final double? height;
final Completer<GoogleMapController> controller;
final MaterialColor indicatorColor;
final Size indicatorSize;
final bool isIndicatorVisible;

 

4. Offset과 angle값을 담는 클래스 생성, 서비스로부터 값 받아오기

구글맵 위경도와 마커 위경도 값을 통해 방향 지시자 위치와 각도를 계산하는 서비스로부터 위치와 각도 값을 가져온다. 편하게 한 객체에 데이터를 담을 수 있도록 클래스 생성

class OffsetWithAngle {
  Offset offset;
  double angle;

  OffsetWithAngle(this.offset, this.angle);
}

 

서비스로부터 이 데이터 형식으로 값을 받아와 렌더링 요청 (간단하게 setStat 사용)

renderIndicator() async {
    List<OffsetWithAngle> offsets = await _service.calculateOffsets(
        widget.controller,
        widget.markers
            .map<LatLng>((marker) =>
                LatLng(marker.position.latitude, marker.position.longitude))
            .toList(),
        widget.indicatorSize);
    if (mounted) {
      setState(() {
        _indicatorOffsetList = offsets;
      });
    }
  }

 

5. Positioned 위젯과 Translate.rotate 위젯으로 표현

Stack위젯 안에서 구글맵 위에 보이도록 구글맵 위젯 아래에 작성.

if (widget.isIndicatorVisible && widget.markers.isNotEmpty)
  ..._indicatorOffsetList.map<Widget>((el) {
    return Positioned(
      left: el.offset.dx,
      top: el.offset.dy,
      child: IgnorePointer(
        ignoring: true,
        child: Transform.rotate(
          angle: -el.angle * math.pi / 180,
          child: Image.asset(
            'assets/images/arrow.png',
            color: Colors.blue,
            height: widget.indicatorSize.height,
            width: widget.indicatorSize.width,
          ),
        ),
      ),
    );
  }).toList(),

 

 

구현 로직 설명 - 서비스 부분

1. 지도 경계선의 위/경도와 지도 중심의 위/경도 구하기

GoolgeMapController.getVisibleRegion 메서드를 활용하여 지도 경계선의 위/경도를 구한 다음 (북쪽 위도와 남쪽 위도의 합 / 2), (서쪽 경도와 동쪽 경도의 합 / 2) 를 통해 지도 중심의 위경도 값을 구한다.

Future<List<OffsetWithAngle>> calculateOffsets(
      Completer<GoogleMapController> mapController,
      List<LatLng> listLatLng,
      Size directionIndicatorSize) async {
    final GoogleMapController controller = await mapController.future;
    LatLngBounds bounds = await controller.getVisibleRegion();

    double latRange = bounds.northeast.latitude - bounds.southwest.latitude;
    double lngRange = bounds.northeast.longitude - bounds.southwest.longitude;

    final LatLng mapCenterLatLng = LatLng(
        bounds.southwest.latitude + latRange / 2.0,
        bounds.southwest.longitude + lngRange / 2.0);

    ...
    
  }

 

2.  마커의 위경도와 비교하여 경계선 밖에 있는 마커 필터링하기

Future<List<OffsetWithAngle>> calculateOffsets(
      Completer<GoogleMapController> mapController,
      List<LatLng> listLatLng,
      Size directionIndicatorSize) async {
 
    ...
    
    List<LatLng> resList = listLatLng
    .where((latLng) => !(latLng.latitude >= bounds.southwest.latitude &&
        latLng.latitude <= bounds.northeast.latitude &&
        latLng.longitude >= bounds.southwest.longitude &&
        latLng.longitude <= bounds.northeast.longitude))
    .toList();
    
  }

 

3.  위젯의 높이와 너비 : 지도의 위경도 의 비율을 구하고 마커와 지도 중심의 위경도의 각도를 구하여 실제 표시해야 할 각도를 구한다.

Future<List<OffsetWithAngle>> calculateOffsets(
      Completer<GoogleMapController> mapController,
      List<LatLng> listLatLng,
      Size directionIndicatorSize) async {
	double latPerPixel = _widgetHeight / latRange;
    double lngPerPixel = _widgetWidth / lngRange;
    
    return resList
        .map((e) => calculateIndicatorPosition(directionIndicatorSize, e,
            mapCenterLatLng, latPerPixel, lngPerPixel))
        .toList();
}

 

3-1.  지도 중심 위경도와 마커 위경도의 각도를 구한다. 

지도 중심 위경도와 마커 위경도의 각도를 구한 뒤

위도 경도가 실제 픽셀 크기로 어떤 비율로 그려지는지 실제 위도 당 픽셀, 경도 당 픽셀 비율을 적용해야 정확한 각도를 구할 수 있다.

  double calculateAngle(LatLng markerLatLng, LatLng centerLatLng,
      double latPerPixel, double lngPerPixel) {
    double deltaX =
        (markerLatLng.longitude - centerLatLng.longitude) * lngPerPixel;
    double deltaY =
        (markerLatLng.latitude - centerLatLng.latitude) * latPerPixel;

    return math.atan2(deltaY, deltaX) * (180 / math.pi);
  }

 

3-3. 삼각함수를 활용하여 위치를 구한다.

a와 c의 접점을 지도의 중심이라 생각하고 c와 b의 접점을 마커의 위치라고 생각하자. 우리가 가지고 있는 값은 a = 위젯의 너비/2, 그리고 θ의 각도이다. 그리고 구해야할 값은 b값이다. 그러므로 b = tanθ * a 이다. 
이제 마커가 몇 분지에 있는지에 따라 b를 다르게 적용하여 offset값을 구한다.

OffsetWithAngle calculateIndicatorPosition(
      Size directionIndicatorSize,
      LatLng markerLatLng,
      LatLng centerLatLng,
      double latPerPixel,
      double lngPerPixel) {
    double angle =
        calculateAngle(markerLatLng, centerLatLng, latPerPixel, lngPerPixel);

    double centerX = _widgetWidth / 2;
    double centerY = _widgetHeight / 2;
    double x, y;

    if (angle >= -_topRightDiagonalAngle && angle <= _topRightDiagonalAngle) {
      x = _widgetWidth - directionIndicatorSize.width;
      y = centerY -
          centerX * math.tan(angle * math.pi / 180) +
          directionIndicatorSize.width / 2 * math.tan(angle * math.pi / 180) -
          directionIndicatorSize.height / 2;
    } else if (angle >= 180 - _topRightDiagonalAngle ||
        angle <= -180 + _topRightDiagonalAngle) {
      x = 0;
      y = centerY +
          centerX * math.tan(angle * math.pi / 180.0) -
          directionIndicatorSize.width / 2 * math.tan(angle * math.pi / 180) -
          directionIndicatorSize.height / 2;
    } else if (angle < 180 - _topRightDiagonalAngle &&
        angle > _topRightDiagonalAngle) {
      y = 0;
      x = centerX -
          centerY * math.tan((angle - 90) * math.pi / 180.0) +
          directionIndicatorSize.width /
              2 *
              math.tan((angle - 90) * math.pi / 180) -
          directionIndicatorSize.height / 2;
    } else {
      y = _widgetHeight - directionIndicatorSize.height;
      x = centerX +
          centerY * math.tan((angle + 90) * math.pi / 180.0) -
          directionIndicatorSize.width /
              2 *
              math.tan((angle + 90) * math.pi / 180.0) -
          directionIndicatorSize.height / 2;
    }
    return OffsetWithAngle(Offset(x, y), angle);
  }