diff --git a/next.config.js b/next.config.js index 147f0e1b..8981a04f 100644 --- a/next.config.js +++ b/next.config.js @@ -6,7 +6,7 @@ module.exports = { PROJECT_ROOT: __dirname, }, images: { - domains: ['storage.googleapis.com'], + domains: ['storage.googleapis.com', 'cloud.maptiler.com'], }, async redirects() { return [ diff --git a/package-lock.json b/package-lock.json index 7e5881ae..a525be87 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "1.0.0", "dependencies": { "@heroicons/react": "^2.1.5", + "@maptiler/sdk": "^2.5.1", "@maptiler/geocoding-control": "^1.4.1", "@nextui-org/react": "^2.4.6", "@phosphor-icons/react": "^2.1.7", @@ -10357,6 +10358,12 @@ "jiti": "bin/jiti.js" } }, + "node_modules/js-base64": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.7.tgz", + "integrity": "sha512-7rCnleh0z2CkXhH67J8K1Ytz0b2Y+yxTPL+/KOJoa20hfnVQ/3/T6W/KflYI4bRHRagNeXeU2bkNGI3v1oS/lw==", + "license": "BSD-3-Clause" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -17522,6 +17529,19 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", diff --git a/package.json b/package.json index f0916682..139b79aa 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "dependencies": { "@heroicons/react": "^2.1.5", + "@maptiler/sdk": "^2.5.1", "@maptiler/geocoding-control": "^1.4.1", "@nextui-org/react": "^2.4.6", "@phosphor-icons/react": "^2.1.7", diff --git a/src/components/MapStyleSwitcher.tsx b/src/components/MapStyleSwitcher.tsx new file mode 100644 index 00000000..0bb4163a --- /dev/null +++ b/src/components/MapStyleSwitcher.tsx @@ -0,0 +1,91 @@ +'use client'; +import React, { useEffect, useState } from 'react'; +import Image from 'next/image'; + +interface MapStyleSwitcherProps { + handleStyleChange: (style: string) => void; +} + +const MapStyleSwitcher: React.FC = ({ + handleStyleChange, +}) => { + const [activeStyle, setActiveStyle] = useState('DATAVIZ'); + const [isHovered, setIsHovered] = useState(false); + type BaseMap = { + name: string; + img: string; + }; + + type BaseMaps = Record; + + const baseMaps: BaseMaps = { + DATAVIZ: { + name: 'DataVisualization', + img: 'https://cloud.maptiler.com/static/img/maps/dataviz.png', + }, + HYBRID: { + name: 'Hybrid', + img: 'https://cloud.maptiler.com/static/img/maps/hybrid.png', + }, + STREETS: { + name: 'Street', + img: 'https://cloud.maptiler.com/static/img/maps/streets.png', + }, + }; + + useEffect(() => { + handleStyleChange(baseMaps[activeStyle].name); + }, [activeStyle]); + + const onClick = (key: string) => { + setActiveStyle(key); + handleStyleChange(baseMaps[key].name); + }; + + return ( +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > + {activeStyle} + +
+ {Object.keys(baseMaps) + .filter((key) => key !== activeStyle) + .map((key) => ( + {key} onClick(key)} + className={`cursor-pointer rounded-md border-2 border-transparent hover:border-gray-400`} + /> + ))} +
+
+ ); +}; + +export default MapStyleSwitcher; diff --git a/src/components/PropertyMap.tsx b/src/components/PropertyMap.tsx index bd50d506..7808bbf4 100644 --- a/src/components/PropertyMap.tsx +++ b/src/components/PropertyMap.tsx @@ -1,5 +1,5 @@ 'use client'; - +import '../components/components-css/PropertyMap.css'; import { FC, useEffect, @@ -25,6 +25,7 @@ import Map, { } from 'react-map-gl/maplibre'; import maplibregl, { Map as MaplibreMap, + IControl, PointLike, MapGeoJSONFeature, ColorSpecification, @@ -49,6 +50,13 @@ import { centroid } from '@turf/centroid'; import { Position } from 'geojson'; import { toTitleCase } from '../utilities/toTitleCase'; import { ThemeButton } from '../components/ThemeButton'; +import MapStyleSwitcher from './MapStyleSwitcher'; + +type MapStyle = { + url: string; +}; + +type MapStyles = Record; type SearchedProperty = { coordinates: [number, number]; @@ -104,15 +112,30 @@ const layerStylePoints: CircleLayerSpecification = { }, }; +const mapStyles: MapStyles = { + DataVisualization: { + url: `https://api.maptiler.com/maps/dataviz/style.json?key=${maptilerApiKey}`, + }, + Hybrid: { + url: `https://api.maptiler.com/maps/hybrid/style.json?key=${maptilerApiKey}`, + }, + Street: { + url: `https://api.maptiler.com/maps/streets/style.json?key=${maptilerApiKey}`, + }, +}; + // info icon in legend summary let summaryInfo: ReactElement | null = null; -const MapControls = () => { +const MapControls: React.FC<{ + handleStyleChange: (styleName: string) => void; +}> = ({ handleStyleChange }) => { const [smallScreenToggle, setSmallScreenToggle] = useState(false); return ( <> + {smallScreenToggle || window.innerWidth > 640 ? ( = ({ const { appFilter } = useFilter(); const [popupInfo, setPopupInfo] = useState(null); const [map, setMap] = useState(null); - const [mapController, setMapController] = useState(); + const [mapController, setMapController] = useState(); + const [currentStyle, setCurrentStyle] = useState( + 'Data Visualization View' + ); const [searchedProperty, setSearchedProperty] = useState({ coordinates: [-75.1628565788269, 39.97008211622267], address: '', @@ -180,6 +206,10 @@ const PropertyMap: FC = ({ handleMapClick(e.lngLat); }; + const handleStyleChange = (styleName: string) => { + setCurrentStyle(styleName); + }; + const moveMap = (targetPoint: LngLatLike) => { if (map) { map.easeTo({ @@ -373,7 +403,7 @@ const PropertyMap: FC = ({ if (map) { updateFilter(); } - }, [map, appFilter]); + }, [map, appFilter, currentStyle]); const changeCursor = (e: any, cursorType: 'pointer' | 'default') => { e.target.getCanvas().style.cursor = cursorType; @@ -385,7 +415,7 @@ const PropertyMap: FC = ({ changeCursor(e, 'pointer')} onMouseLeave={(e) => changeCursor(e, 'default')} onClick={onMapClick} @@ -409,8 +439,20 @@ const PropertyMap: FC = ({ onSourceData={(e) => { handleSetFeatures(e); }} + onStyleData={(e) => { + const layerIds = e.target + .getStyle() + .layers.map((layer: any) => layer.id); + const layersApplied = layers.every((layer) => + layerIds.includes(layer) + ); + if (layersApplied) { + setHasLoadingError(false); + } + }} onMoveEnd={handleSetFeatures} > +
= ({ }} />
- {popupInfo && (