imaginerio/imaginerioNext

View on GitHub
components/AtlasController/index.js

Summary

Maintainability
A
2 hrs
Test Coverage
import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import axios from 'axios';
import useSWR from 'swr';
import { useRouter } from 'next/router';
import { flatten } from 'lodash';
import { Atlas } from '@imaginerio/diachronic-atlas';
import { Box, HStack, Checkbox, Text } from '@chakra-ui/react';

import Legend from '../Legend';
import Probe from '../Probe';
import OpacityControl from '../OpacityControl';
import HeadingControl from '../HeadingControl';
import ViewControl from '../ViewControl';

import mapStyle from '../../assets/style/style.json';

import { useImages } from '../../providers/ImageContext';
import useDebouncedEffect from '../../utils/useDebouncedEffect';
import translation from '../../assets/config/translations';

const fetcher = ssid => {
  if (ssid) {
    return axios
      .get(`${process.env.NEXT_PUBLIC_SEARCH_API}/document/${ssid}`)
      .then(({ data }) => data);
  }
  return { data: null };
};

const buttonPosition = {
  pos: 'absolute',
  right: '15px',
  top: '240px',
  zIndex: 9,
};

const AtlasController = ({ width, height, mobile }) => {
  const [
    {
      activeImages,
      year,
      selectedImage,
      allImages,
      showViewPoints,
      highlightedLayer,
      highlightedFeature,
      yearDragging,
      drawSearch,
      mapBounds,
    },
    dispatch,
  ] = useImages();

  const router = useRouter();
  useEffect(() => {
    const hash = router.asPath.split('#')[1];
    if (hash) {
      const newImage = allImages.find(image => image.ssid === hash);
      if (newImage) {
        dispatch(['SET_SELECTED_IMAGE', newImage]);
      }
    }
  }, []);

  const [geojson, setGeojson] = useState([]);
  const [viewCone, setViewCone] = useState(null);
  const [viewpoints, setViewpoints] = useState(activeImages.filter(i => i.collection === 'views'));
  const [featureJson, setFeatureJson] = useState(null);
  const [hoverSSID, setHoverSSID] = useState(null);
  const [probeImage, setProbeImage] = useState(null);
  const [probePosition, setProbePosition] = useState(null);
  const [opacity, setOpacity] = useState(1);
  const [heading, setHeading] = useState(0);
  const [searchMove, setSearchMove] = useState(false);
  const [mapBBox, setMapBBox] = useState(null);

  const { data: hover } = useSWR(hoverSSID, fetcher);

  useEffect(() => {
    if (selectedImage) {
      axios
        .get(`${process.env.NEXT_PUBLIC_SEARCH_API}/document/${selectedImage.ssid}`)
        .then(({ data }) =>
          setViewCone({
            id: selectedImage.ssid,
            data,
            type: 'fill',
            paint: {
              'fill-color':
                selectedImage.collection === 'views' ? 'rgba(0,0,0,0.25)' : 'rgba(0,0,0,0)',
            },
          })
        );
      router.replace(`${router.basePath}#${selectedImage.ssid}`);
    } else {
      setViewCone(null);
      router.replace(router.basePath);
    }
  }, [selectedImage]);

  useEffect(() => {
    if (highlightedFeature) {
      axios
        .get(`${process.env.NEXT_PUBLIC_SEARCH_API}/feature/${highlightedFeature}?year=${year}`)
        .then(({ data }) => {
          let geometry = data.geometry.type;
          if (geometry === 'GeometryCollection') geometry = data.geometry.geometries[0].type;
          let type;
          const paint = {};
          switch (geometry) {
            case 'Point':
              type = 'circle';
              break;
            case 'LineString':
            case 'MultiLineString':
              type = 'line';
              paint['line-width'] = 2;
              break;
            case 'Polygon':
            case 'MultiPolygon':
              type = 'fill';
              paint['fill-color'] = 'rgba(0,0,0,0.5)';
              break;
            default:
              type = 'fill';
          }
          return setFeatureJson({ id: highlightedFeature, data, type, paint });
        });
    } else {
      setFeatureJson(null);
    }
  }, [highlightedFeature]);

  useEffect(() => {
    const newViewpoints = activeImages.filter(i => i.collection === 'views');
    if (newViewpoints.length !== viewpoints.length) {
      setViewpoints(newViewpoints);
    }
  }, [activeImages]);

  useEffect(() => {
    const geo = [];
    if (viewCone) geo.push(viewCone);
    if (featureJson) geo.push(featureJson);
    setGeojson(geo);
  }, [viewCone, featureJson]);

  useEffect(() => {
    if (hoverSSID) {
      setProbeImage(activeImages.find(i => i.ssid === hoverSSID));
    } else {
      setProbeImage(null);
    }
  }, [hoverSSID]);

  useDebouncedEffect(
    () => {
      if (searchMove) {
        dispatch(['SET_MAP_BOUNDS', mapBBox]);
      } else if (mapBounds) {
        dispatch(['SET_MAP_BOUNDS', null]);
      }
    },
    [searchMove, mapBBox],
    500
  );

  return (
    <Box>
      <Legend />
      <Atlas
        year={year}
        mapStyle={mapStyle}
        viewport={{
          longitude: !Number.isNaN(parseFloat(router.query.lng)) ? +router.query.lng : -43.18,
          latitude: !Number.isNaN(parseFloat(router.query.lat)) ? +router.query.lat : -22.9,
          zoom: !Number.isNaN(parseFloat(router.query.zoom)) ? +router.query.zoom : 14.5,
        }}
        width={width}
        height={height}
        viewpoints={showViewPoints && !yearDragging ? viewpoints : null}
        activeBasemap={selectedImage && selectedImage.collection !== 'views' && selectedImage.ssid}
        geojson={geojson}
        rasterUrl={process.env.NEXT_PUBLIC_RASTER_URL}
        basemapHandler={ssid => {
          if (mobile) {
            window.open(`/iconography/views/${ssid}`);
          } else {
            dispatch(['SET_SELECTED_IMAGE', allImages.find(i => i.ssid === ssid)]);
          }
        }}
        loadHandler={() => dispatch(['SET_SHOW_INTRO', true])}
        circleMarkers
        hover={hover}
        opacity={opacity}
        highlightedLayer={highlightedLayer}
        bearing={heading}
        isDrawing={drawSearch === 'box'}
        drawBoxHandler={e => dispatch(['SET_DRAW_SEARCH_COORDS', flatten(e)])}
        clickHandler={e => {
          if (drawSearch === 'click') dispatch(['SET_DRAW_SEARCH_COORDS', e]);
        }}
        showSatellite={year === new Date().getFullYear()}
        maxZoom={18}
        hoverHandler={e => {
          if (e.features.length && !mobile) {
            setProbePosition(e.center);
            setHoverSSID(e.features[0].properties.ssid);
          } else {
            setHoverSSID(null);
          }
        }}
        bboxHandler={setMapBBox}
      />
      {!mobile && (
        <HStack
          pos="absolute"
          top="105px"
          right="55px"
          zIndex={9}
          bgColor="white"
          p={2}
          borderRadius={4}
          boxShadow="sm"
          onClick={() => setSearchMove(!searchMove)}
          cursor="pointer"
        >
          <Checkbox isChecked={searchMove} pointerEvents="none" />
          <Text>{translation.searchMoves[router.locale]}</Text>
        </HStack>
      )}
      <ViewControl
        {...buttonPosition}
        handler={() => dispatch(['TOGGLE_VIEWPOINTS'])}
        show={showViewPoints}
        top="200px"
      />
      {selectedImage && selectedImage.collection !== 'views' && (
        <OpacityControl {...buttonPosition} opacity={opacity} handler={setOpacity} />
      )}
      {viewCone && viewCone.data.features[0].properties.heading && (
        <HeadingControl
          {...buttonPosition}
          targetHeading={viewCone.data.features[0].properties.heading}
          heading={heading}
          handler={setHeading}
        />
      )}
      {probeImage && <Probe image={probeImage} pos={probePosition} />}
    </Box>
  );
};

AtlasController.propTypes = {
  width: PropTypes.number.isRequired,
  height: PropTypes.number.isRequired,
  mobile: PropTypes.bool,
};

AtlasController.defaultProps = {
  mobile: false,
};

export default AtlasController;