Airbnbのようにマップを作成する方法とGoogleマップ



導入

免責事項
  • 英語は私の母国語ではないので、テキストに誤りがあるかもしれません、しかし、私はコードが私の語
  • のどれよりも1000倍も言うことができると確信します
  • 私は、解決策を柔軟に理解し、可能な限り
  • として反応マップライブラリを使用しなかった
  • これは、ディープスタイリング、クラスタリングとグローバルストレージなしで最も簡単な実装です.
  • のソースコードはここにある
  • https://github.com/alex1998dmit/map_airbnb
    タスク
    それがAirbnbの上でされるように、我々がアパートカードをそれに表示している地図をつくる必要があると仮定しましょう
    次のテクノロジスタックです.
  • 反応とタイプスクリプト
  • Googleマップと@ Googlemaps/反応ラッパー
  • スタイルスタッフのための
  • Mui

    実装

    アプリを作成
    すべてはかなり些細な-あなたはアプリケーションをインストールする必要があります
    npx create-react-app my-app --template typescript
    

    インストール依存
    アプリケーションが動作するには、私たちは、mui @ gogglemaps/反応ラッパーが必要です
    npm install --save @material-ui/core @material-ui/icons @googlemaps/react-wrapper
    

    設定マップ
    このステップでは、アプリケーションに簡単なマップを統合します.まず最初に、Googleマップキーを得る必要があります.
    まず、Googleマップのラッパーになるマップコンポーネントを作成しましょう.
    import { useEffect, useRef, useState } from "react";
    // we will use make styles for styling components, you can use another solutions (like css, sass or cssonjs
    import { makeStyles } from "@material-ui/core";
    // api mock data
    import Apartments from "./apartments";
    
    // Our component will receive center coords and zoom size in props
    type MapProps = {
      center: google.maps.LatLngLiteral
      zoom: number
    }
    
    // map wrapper styles
    const useStyles = makeStyles({
      map: {
        height: '100vh'
      }
    })
    
    function Map({ center, zoom }: MapProps) {
      const ref = useRef(null);
      const [map, setMap] = useState<google.maps.Map<Element> | null>(null)
      const classes = useStyles();
    
      useEffect(() => {
        // we need to save google-map object for adding markers and routes in future
        if (ref.current) {
          // here will connect map frame to div element in DOM by using ref hook
          let createdMap = new window.google.maps.Map(
            ref.current,
            {
              center,
              zoom,
              disableDefaultUI: true,
              clickableIcons: false
            }
          );
          setMap(createdMap)
        }
      }, [center, zoom]);
    
      // map will be connect to this div block
      return <div ref={ref} id="map" className={classes.map} />;
    }
    
    export default Map
    
    次に、アプリを変更してみましょう.tsx with:
    import React, { ReactElement } from 'react';
    import { Wrapper, Status } from "@googlemaps/react-wrapper";
    import Map from './Map'
    
    // Here we can add views when map will loading or failure
    const render = (status: Status): ReactElement => {
      if (status === Status.LOADING) return <h3>{status} ..</h3>;
      if (status === Status.FAILURE) return <h3>{status} ...</h3>;
      return <></>;
    };
    
    function App() {
      if (!process.env.REACT_APP_GOOGLE_KEY) {
        return <h2>Add google key</h2>
      }
      return (
        <div className="App">
          <Wrapper apiKey={process.env.REACT_APP_GOOGLE_KEY} render={render}>
            <Map center={{ lat: 55.753559, lng: 37.609218 }} zoom={11} />
          </Wrapper>
        </div>
      );
    }
    
    export default App;
    
    結果は以下の通りです:

    https://developers.google.com/maps/documentation/javascript/get-api-key
    カスタムオーバーレイを追加
    次のステップはカスタムオーバーレイを追加することです.なぜ我々はマーカーを使用しないオーバーレイを使用しますか?私の意見では、定期的なマーカーをカスタマイズするのは難しいでしょう、ドキュメントによって、私たちはアイコンイメージだけを変えることができて、それの上にラベルをつけることができます.
    重ね合わせコンテナーを作成しましょう.そして、それは特定の座標で地図に位置するコンポーネントのためのラッパーであるでしょう.
    import * as React from 'react'
    import ReactDOM from 'react-dom';
    
    // base function for creating DOM div node
    function createOverlayElement() {
      const el = document.createElement('div');
      el.style.position = 'absolute';
      el.style.display = 'inline-block';
      el.style.width = '9999px';
      return el;
    }
    
    // Our OverlayComponent will recieve map, postion and children props - position is coords, map is google.map object and children is a component that will be render in overlay
    export type Props = {
      map: google.maps.Map | null
      position: { lat: number, lng: number }
      children?: React.ReactChild
    }
    
    const OverlayContainer = (props: Props) => {
      const overlay = React.useRef<google.maps.OverlayView | null>(null)
      const el = React.useRef<Element | null>(null)
    
      // modified OverlayView from google.maps [https://developers.google.com/maps/documentation/javascript/reference/3.44/overlay-view?hl=en]
      class OverlayView extends window.google.maps.OverlayView {
        position: google.maps.LatLng | null = null;
        content: any = null;
    
        constructor(props: any) {
          super();
          props.position && (this.position = props.position);
          props.content && (this.content = props.content);
        }
    
        onAdd = () => {
          if (this.content) this.getPanes().floatPane.appendChild(this.content);
        };
    
        onRemove = () => {
          if (this.content?.parentElement) {
            this.content.parentElement.removeChild(this.content);
          }
        };
    
        draw = () => {
          if (this.position) {
            const divPosition = this.getProjection().fromLatLngToDivPixel(
              this.position
            );
            this.content.style.left = divPosition.x + 'px';
            this.content.style.top = divPosition.y + 'px';
          }
        };
      }
    
      React.useEffect(() => {
        return () => {
          if (overlay.current) overlay.current.setMap(null)
        }
      }, [])
    
      if (props.map) {
        el.current = el.current || createOverlayElement()
        overlay.current = overlay.current || new OverlayView(
          {
            position: new google.maps.LatLng(props.position.lat, props.position.lng),
            content: el.current
          }
        )
        overlay.current.setMap(props.map)
        return ReactDOM.createPortal(props.children, el.current);
      }
      return null
    }
    
    export default OverlayContainer
    

    マップポイントとアパートカードの作成
    私は、Mui CoreとMUIアイコンを使用して簡単なアパートカードを作成します.
    カードを作成しましょう
    import React from 'react';
    import { makeStyles } from '@material-ui/core/styles';
    import Card from '@material-ui/core/Card';
    import CardActionArea from '@material-ui/core/CardActionArea';
    import CardContent from '@material-ui/core/CardContent';
    import CardMedia from '@material-ui/core/CardMedia';
    import Typography from '@material-ui/core/Typography';
    import AspectRatioIcon from '@material-ui/icons/AspectRatio';
    import { Grid, IconButton } from '@material-ui/core';
    import MeetingRoomIcon from '@material-ui/icons/MeetingRoom';
    import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
    import CloseIcon from '@material-ui/icons/Close';
    
    const useStyles = makeStyles({
      root: {
        maxWidth: 230,
        position: 'relative',
        zIndex: 1001,
      },
      media: {
        height: 100,
      },
      close: {
        position: 'absolute',
        left: 0,
        top: 0,
        zIndex: 1001,
        background: 'white',
        width: '25px',
        height: '25px'
      }
    });
    
    type ApartmentCardProps = {
      image: string
      address: string
      area: number
      rooms_number: number
      floor: number
      floor_count: number
      rent: number
      handleClose: () => void
    }
    
    export default function ApartmentCard(props: ApartmentCardProps) {
      const classes = useStyles();
      return (
        <Card className={classes.root}>
           <IconButton className={classes.close} aria-label="close" onClick={props.handleClose}>
            <CloseIcon />
          </IconButton>
          <CardActionArea>
            <CardMedia
              className={classes.media}
              image={props.image}
              title="Contemplative Reptile"
            />
            <CardContent>
              <Typography variant="body2" component="h2">
                {props.address}
              </Typography>
              <Grid container spacing={1}>
                <Grid item container xs={6} spacing={1} alignItems='center'>
                  <Grid item xs={8}><AspectRatioIcon /></Grid>
                  <Grid item xs={4}>{props.area}</Grid>
                </Grid>
                <Grid item container xs={6} spacing={1} alignItems='center'>
                  <Grid item xs={8}><MeetingRoomIcon /></Grid>
                  <Grid item xs={4}>{props.rooms_number}</Grid>
                </Grid>
                <Grid item container xs={6} spacing={1} alignItems='center'>
                  <Grid item xs={8}><KeyboardArrowUpIcon /></Grid>
                  <Grid item xs={4}>{props.floor}/{props.floor_count}</Grid>
                </Grid>
                <Grid item container xs={12} spacing={1} alignItems='center' justifyContent="center">
                  <Typography variant="body2" style={{ fontWeight: 600 }}>{props.rent} $</Typography>
                </Grid>
              </Grid>
              </Typography> */}
            </CardContent>
          </CardActionArea>
        </Card>
      );
    }
    
    と接続点:
    import { makeStyles } from "@material-ui/styles"
    
    type ApartmentPonitProps = {
      price: number
      onClick: () => void
    }
    
    const styles = makeStyles({
      root:{
        background: 'white',
        borderRadius: '12px',
        padding: '8px',
        width: '60px',
        zIndex: 1000,
        position: 'relative'
      }
    })
    
    const ApartmentPoint = (props: ApartmentPonitProps) => {
      const classes = styles()
      return (
        <div className={classes.root} onClick={props.onClick}>
          {props.price} $
        </div>
      )
    }
    
    export default ApartmentPoint
    
    ApammentPointまたはAtsolmentCardをレンダリングするmappoint - likeラッパーを使用します.
    import { useEffect, useRef, useState } from "react"
    import ApartmentCard from "./ApartmentCard"
    import ApartmentPoint from "./ApartmentPoint"
    
    type MapPointProps = {
      image: string
      address: string
      area: number
      rooms_number: number
      floor: number
      floor_count: number
      rent: number
    }
    
    const MapPoint = (props: MapPointProps) => {
      const [opened, setIsOpened] = useState<boolean>(false)
      const handleOnOpen = () => setIsOpened(true)
      const handleOnClose = () => setIsOpened(false)
      const containerRef = useRef<HTMLDivElement>(null)
    
    // Hook for handle outside click - simple implementation from stack overflow
      useEffect(() => {
        function handleClickOutside(this: Document, event: MouseEvent) {
          if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
            setIsOpened(false)
          }
        }
    
        document.addEventListener("mousedown", handleClickOutside);
        return () => {
          document.removeEventListener("mousedown", handleClickOutside);
        };
      }, [containerRef]);
    
      return (<div ref={containerRef}>
        {opened ?
          <ApartmentCard
            image={props.image}
            address={props.address}
            area={props.area}
            rooms_number={props.rooms_number}
            floor={props.floor}
            floor_count={props.floor_count}
            rent={props.rent}
            handleClose={handleOnClose}
          /> :
          <ApartmentPoint
            price={props.rent}
            onClick={handleOnOpen}
          />}
      </div>)
    }
    
    export default MapPoint
    

    すべて一緒に
    オーバーレイコンテナ内のアパートメントポイントを追加して、マップコンポーネントを変更します
    import { makeStyles } from "@material-ui/core";
    import { useEffect, useRef, useState } from "react";
    import Apartments from "./apartments";
    import MapPoint from "./MapPoint";
    import OverlayContainer from "./OverlayContainer";
    
    type MapProps = {
      center: google.maps.LatLngLiteral
      zoom: number
    }
    
    const useStyles = makeStyles({
      map: {
        height: '100vh'
      }
    })
    
    function Map({ center, zoom }: MapProps) {
      const ref = useRef(null);
      const [map, setMap] = useState<google.maps.Map<Element> | null>(null)
      const classes = useStyles();
    
      useEffect(() => {
        if (ref.current) { 
          let createdMap = new window.google.maps.Map(
            ref.current,
            {
              center,
              zoom,
              disableDefaultUI: true,
              clickableIcons: false
            }
          );
          setMap(createdMap)
        }
      }, [center, zoom]);
    
      return <div ref={ref} id="map" className={classes.map}>
        {Apartments.map((apartment, index) => (
          <OverlayContainer
            map={map}
            position={{
              lat: apartment.lat,
              lng: apartment.lng
            }}
            key={index}
          >
            <MapPoint
              image={apartment.image}
              address={apartment.address}
              area={apartment.area}
              rooms_number={apartment.rooms_number}
              floor={apartment.floor}
              floor_count={apartment.floor_count}
              rent={apartment.rent}
            />
          </OverlayContainer>
        ))}
      </div>;
    }
    
    export default Map
    
    アパートの模擬データの例(アパートメント).
    const Apartments = [
      {
        "id": 1,
        "image": "https://storage.yandexcloud.net/apartment-images/2.jpg",
        "area": 34.9,
        "kitchen_area": null,
        "address": "Novoalekseevskaya  4d4",
        "lat": 55.80562399999999,
        "lng": 37.641239,
        "rooms_number": 1,
        "bedrooms_number": 1,
        "restrooms_number": 1,
        "floor": 3,
        "floor_count": 14,
        "rent": 1500
      },
      {
        "id": 2,
        "image": "https://storage.yandexcloud.net/apartment-images/10_S939Rcf.jpg",
        "area": 47,
        "kitchen_area": null,
        "address": "Valovaya street 31",
        "lat": 55.66497999999999,
        "lng": 37.857464,
        "rooms_number": 1,
        "bedrooms_number": 1,
        "restrooms_number": 1,
        "floor": 6,
        "floor_count": 9,
        "rent": 2000
      },
      {
        "id": 3,
        "image": "https://storage.yandexcloud.net/apartment-images/07_uvV7gIk.jpg",
        "area": 40.9,
        "kitchen_area": null,
        "address": "academic Volgyn street 8A",
        "lat": 55.68271799999999,
        "lng": 37.544263,
        "rooms_number": 3,
        "bedrooms_number": 2,
        "restrooms_number": 1,
        "floor": 2,
        "floor_count": 5,
        "rent": 3000
      }
    ]
    
    export default Apartments
    
    https://developers.google.com/maps/documentation/javascript/custom-markers
    結果
    そして、我々のアプリケーションは以下の通りです.

    https://material-ui.com/ru/components/cards/
    ピーエス
    これは、最初の記事では、私はどのようにGoogleマップとどのように動作し、さらに記事では、より多くのロジックとスタイルを可能な限り近くのBirbnb