From 46caf8b56e4c176e21a74ad7f349f6bb7595162d Mon Sep 17 00:00:00 2001 From: Donovan Date: Sat, 14 Dec 2024 15:45:30 -0500 Subject: [PATCH 1/2] Addes map style button --- .eslintrc.json | 1 + .../find-properties/[[...opa_id]]/page.tsx | 1 - src/components/PropertyDetailSection.tsx | 13 +-- src/components/PropertyMap.tsx | 80 ++++++++++++++++++- src/components/components-css/PropertyMap.css | 16 ++++ 5 files changed, 94 insertions(+), 17 deletions(-) create mode 100644 src/components/components-css/PropertyMap.css diff --git a/.eslintrc.json b/.eslintrc.json index 0fd6bf39..fd33f44a 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -5,6 +5,7 @@ "sourceType": "module", "project": "./tsconfig.json" }, + "root": true, "extends": [ "next/core-web-vitals", "plugin:react/recommended", diff --git a/src/app/find-properties/[[...opa_id]]/page.tsx b/src/app/find-properties/[[...opa_id]]/page.tsx index c7a1f809..7fb0e463 100644 --- a/src/app/find-properties/[[...opa_id]]/page.tsx +++ b/src/app/find-properties/[[...opa_id]]/page.tsx @@ -294,7 +294,6 @@ const MapPage = ({ params }: MapPageProps) => { featuresInView={featuresInView} display={currentView as 'detail' | 'list'} loading={loading} - hasLoadingError={hasLoadingError} selectedProperty={selectedProperty} setSelectedProperty={setSelectedProperty} setIsStreetViewModalOpen={setIsStreetViewModalOpen} diff --git a/src/components/PropertyDetailSection.tsx b/src/components/PropertyDetailSection.tsx index a6de9a83..0c5ee28e 100644 --- a/src/components/PropertyDetailSection.tsx +++ b/src/components/PropertyDetailSection.tsx @@ -42,7 +42,6 @@ interface PropertyDetailSectionProps { featuresInView: MapGeoJSONFeature[]; display: 'detail' | 'list'; loading: boolean; - hasLoadingError: boolean; selectedProperty: MapGeoJSONFeature | null; setSelectedProperty: (property: MapGeoJSONFeature | null) => void; setIsStreetViewModalOpen: Dispatch>; @@ -56,7 +55,6 @@ const PropertyDetailSection: FC = ({ featuresInView, display, loading, - hasLoadingError, selectedProperty, setSelectedProperty, setIsStreetViewModalOpen, @@ -166,16 +164,7 @@ const PropertyDetailSection: FC = ({ return featuresInView.slice(start, end); }, [page, featuresInView, smallScreenMode]); - return hasLoadingError ? ( -
-
-

We are having technical issues.

-
-
-

Please try again later.

-
-
- ) : loading ? ( + return loading ? (
{/* Center vertically in screen */}
diff --git a/src/components/PropertyMap.tsx b/src/components/PropertyMap.tsx index c1d636dd..01c3fef4 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, @@ -46,7 +46,7 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import { MapLegendControl } from './MapLegendControl'; import { createPortal } from 'react-dom'; import { Tooltip } from '@nextui-org/react'; -import { Info, X } from '@phosphor-icons/react'; +import { Info, MapPinArea, X } from '@phosphor-icons/react'; import { centroid } from '@turf/centroid'; import { Position } from 'geojson'; import { toTitleCase } from '../utilities/toTitleCase'; @@ -106,6 +106,21 @@ const layerStylePoints: CircleLayerSpecification = { }, }; +const mapStyles = [ + { + name: 'Data Visualization View', + url: `https://api.maptiler.com/maps/dataviz/style.json?key=${maptilerApiKey}`, + }, + { + name: 'Sattelite View', + url: `https://api.maptiler.com/maps/hybrid/style.json?key=${maptilerApiKey}`, + }, + { + name: 'Street View', + url: `https://api.maptiler.com/maps/streets/style.json?key=${maptilerApiKey}`, + }, +]; + // info icon in legend summary let summaryInfo: ReactElement | null = null; @@ -164,6 +179,7 @@ const PropertyMap: FC = ({ const { appFilter } = useFilter(); const [popupInfo, setPopupInfo] = useState(null); const [map, setMap] = useState(null); + const [currentStyle, setCurrentStyle] = useState(0); const geocoderRef = useRef(null); const [searchedProperty, setSearchedProperty] = useState({ coordinates: [-75.1628565788269, 39.97008211622267], @@ -183,6 +199,10 @@ const PropertyMap: FC = ({ handleMapClick(e.lngLat); }; + const handleStyleChange = () => { + setCurrentStyle((prevStyle) => (prevStyle + 1) % mapStyles.length); + }; + const moveMap = (targetPoint: LngLatLike) => { if (map) { map.easeTo({ @@ -421,19 +441,64 @@ const PropertyMap: FC = ({ if (map) { updateFilter(); } - }, [map, appFilter]); + }, [map, appFilter, currentStyle]); const changeCursor = (e: any, cursorType: 'pointer' | 'default') => { e.target.getCanvas().style.cursor = cursorType; }; + map?.on('load', () => { + console.log('Map loaded, checking layers...'); + + if (!map.getLayer('vacant_properties_tiles_points')) { + map.addLayer(layerStylePoints); + } + if (!map.getLayer('vacant_properties_tiles_polygons')) { + map.addLayer(layerStylePolygon); + } + + if ( + map.getLayer('vacant_properties_tiles_points') && + map.getLayer('vacant_properties_tiles_polygons') + ) { + console.log('Both layers found, applying filters...'); + + const mapFilter = Object.entries(appFilter).reduce( + (acc, [property, filterItem]) => { + if (filterItem.values.length) { + const thisFilterGroup: any = ['any']; + filterItem.values.forEach((item) => { + if (filterItem.useIndexOfFilter) { + thisFilterGroup.push([ + '>=', + ['index-of', item, ['get', property]], + 0, + ]); + } else { + thisFilterGroup.push(['in', ['get', property], item]); + } + }); + acc.push(thisFilterGroup); + } + return acc; + }, + [] as any[] + ); + + map.setFilter('vacant_properties_tiles_points', ['all', ...mapFilter]); + map.setFilter('vacant_properties_tiles_polygons', ['all', ...mapFilter]); + } else { + console.warn('Layers not found, skipping filter application.'); + } + }); + // map load return (
changeCursor(e, 'pointer')} onMouseLeave={(e) => changeCursor(e, 'default')} onClick={onMapClick} @@ -452,6 +517,13 @@ const PropertyMap: FC = ({ onMoveEnd={handleSetFeatures} > + {popupInfo && ( Date: Tue, 17 Dec 2024 14:29:28 -0500 Subject: [PATCH 2/2] Now uses maptiler switch --- next.config.js | 2 +- package-lock.json | 55 ++++++++++++ package.json | 17 ++-- .../find-properties/[[...opa_id]]/page.tsx | 1 + src/components/MapStyleSwitcher.tsx | 84 +++++++++++++++++++ src/components/PropertyDetailSection.tsx | 13 ++- src/components/PropertyMap.tsx | 48 +++++------ src/components/index.ts | 1 + 8 files changed, 187 insertions(+), 34 deletions(-) create mode 100644 src/components/MapStyleSwitcher.tsx 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 eab41f1c..4fca57f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@heroicons/react": "^2.1.5", "@mapbox/mapbox-gl-geocoder": "^5.0.2", + "@maptiler/sdk": "^2.5.1", "@nextui-org/react": "^2.4.6", "@phosphor-icons/react": "^2.1.7", "@turf/centroid": "^7.0.0", @@ -2015,6 +2016,41 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-3.0.0.tgz", "integrity": "sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==" }, + "node_modules/@maptiler/client": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@maptiler/client/-/client-2.2.0.tgz", + "integrity": "sha512-kV4dSJK2PLfRLnl437CQgDJBboHcf+Z7FWkSoPW3ANca/csoYQQOwz42BPNkda/98OT+CviIueeQdNUyeEL1OQ==", + "license": "BSD-3-Clause", + "dependencies": { + "quick-lru": "^7.0.0" + } + }, + "node_modules/@maptiler/client/node_modules/quick-lru": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-7.0.0.tgz", + "integrity": "sha512-MX8gB7cVYTrYcFfAnfLlhRd0+Toyl8yX8uBx1MrX7K0jegiz9TumwOK27ldXrgDlHRdVi+MqU9Ssw6dr4BNreg==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@maptiler/sdk": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@maptiler/sdk/-/sdk-2.5.1.tgz", + "integrity": "sha512-JKFzSjsDTkbJhqshtPwkl6P3Yf5XZzdbfUQZdSutQb6BatR9kuxhdX5in9A3vHTUjgjTwoQQ4pCvhvK7cnAuGA==", + "license": "BSD-3-Clause", + "dependencies": { + "@maplibre/maplibre-gl-style-spec": "^20.3.1", + "@maptiler/client": "^2.2.0", + "events": "^3.3.0", + "js-base64": "^3.7.4", + "maplibre-gl": "4.7.1", + "uuid": "^9.0.0" + } + }, "node_modules/@next/env": { "version": "14.2.18", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.18.tgz", @@ -10671,6 +10707,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", @@ -18197,6 +18239,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 ad430062..1ca2fca6 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,7 @@ "dependencies": { "@heroicons/react": "^2.1.5", "@mapbox/mapbox-gl-geocoder": "^5.0.2", + "@maptiler/sdk": "^2.5.1", "@nextui-org/react": "^2.4.6", "@phosphor-icons/react": "^2.1.7", "@turf/centroid": "^7.0.0", @@ -37,14 +38,6 @@ "typescript": "5.5.3" }, "devDependencies": { - "@typescript-eslint/eslint-plugin": "^7.16.1", - "@typescript-eslint/parser": "^7.16.1", - "eslint": "^8.56.0", - "eslint-config-next": "^14.2.5", - "eslint-config-prettier": "^9.1.0", - "eslint-plugin-custom-rules": "file:./eslint-plugin-custom-rules", - "eslint-plugin-react": "^7.34.4", - "eslint-plugin-prettier": "^5.0.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.0", "@semantic-release/git": "^10.0.1", @@ -57,6 +50,14 @@ "@types/pg": "^8.11.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", + "@typescript-eslint/eslint-plugin": "^7.16.1", + "@typescript-eslint/parser": "^7.16.1", + "eslint": "^8.56.0", + "eslint-config-next": "^14.2.5", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-custom-rules": "file:./eslint-plugin-custom-rules", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.34.4", "postcss-nesting": "^12.1.5", "postcss-preset-env": "^9.6.0", "semantic-release": "^24.0.0" diff --git a/src/app/find-properties/[[...opa_id]]/page.tsx b/src/app/find-properties/[[...opa_id]]/page.tsx index 7fb0e463..c7a1f809 100644 --- a/src/app/find-properties/[[...opa_id]]/page.tsx +++ b/src/app/find-properties/[[...opa_id]]/page.tsx @@ -294,6 +294,7 @@ const MapPage = ({ params }: MapPageProps) => { featuresInView={featuresInView} display={currentView as 'detail' | 'list'} loading={loading} + hasLoadingError={hasLoadingError} selectedProperty={selectedProperty} setSelectedProperty={setSelectedProperty} setIsStreetViewModalOpen={setIsStreetViewModalOpen} diff --git a/src/components/MapStyleSwitcher.tsx b/src/components/MapStyleSwitcher.tsx new file mode 100644 index 00000000..6484e337 --- /dev/null +++ b/src/components/MapStyleSwitcher.tsx @@ -0,0 +1,84 @@ +'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); + useEffect(() => { + console.log('MapStyleSwitcher rendered', handleStyleChange); + }); + const baseMaps = { + STREETS: { + name: 'Street', + img: 'https://cloud.maptiler.com/static/img/maps/streets.png', + }, + 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', + }, + }; + + const onClick = (key: string) => { + setActiveStyle(key); + console.log(handleStyleChange); + 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/PropertyDetailSection.tsx b/src/components/PropertyDetailSection.tsx index 0c5ee28e..a6de9a83 100644 --- a/src/components/PropertyDetailSection.tsx +++ b/src/components/PropertyDetailSection.tsx @@ -42,6 +42,7 @@ interface PropertyDetailSectionProps { featuresInView: MapGeoJSONFeature[]; display: 'detail' | 'list'; loading: boolean; + hasLoadingError: boolean; selectedProperty: MapGeoJSONFeature | null; setSelectedProperty: (property: MapGeoJSONFeature | null) => void; setIsStreetViewModalOpen: Dispatch>; @@ -55,6 +56,7 @@ const PropertyDetailSection: FC = ({ featuresInView, display, loading, + hasLoadingError, selectedProperty, setSelectedProperty, setIsStreetViewModalOpen, @@ -164,7 +166,16 @@ const PropertyDetailSection: FC = ({ return featuresInView.slice(start, end); }, [page, featuresInView, smallScreenMode]); - return loading ? ( + return hasLoadingError ? ( +
+
+

We are having technical issues.

+
+
+

Please try again later.

+
+
+ ) : loading ? (
{/* Center vertically in screen */}
diff --git a/src/components/PropertyMap.tsx b/src/components/PropertyMap.tsx index 01c3fef4..ae04f967 100644 --- a/src/components/PropertyMap.tsx +++ b/src/components/PropertyMap.tsx @@ -46,11 +46,12 @@ import '@mapbox/mapbox-gl-geocoder/dist/mapbox-gl-geocoder.css'; import { MapLegendControl } from './MapLegendControl'; import { createPortal } from 'react-dom'; import { Tooltip } from '@nextui-org/react'; -import { Info, MapPinArea, X } from '@phosphor-icons/react'; +import { Info, X } from '@phosphor-icons/react'; import { centroid } from '@turf/centroid'; import { Position } from 'geojson'; import { toTitleCase } from '../utilities/toTitleCase'; import { ThemeButton } from '../components/ThemeButton'; +import MapStyleSwitcher from './MapStyleSwitcher'; type SearchedProperty = { coordinates: [number, number]; @@ -106,31 +107,31 @@ const layerStylePoints: CircleLayerSpecification = { }, }; -const mapStyles = [ - { - name: 'Data Visualization View', +const mapStyles = { + DataVisualization: { url: `https://api.maptiler.com/maps/dataviz/style.json?key=${maptilerApiKey}`, }, - { - name: 'Sattelite View', + Hybrid: { url: `https://api.maptiler.com/maps/hybrid/style.json?key=${maptilerApiKey}`, }, - { - name: 'Street View', + 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 [currentStyle, setCurrentStyle] = useState(0); + const [currentStyle, setCurrentStyle] = useState( + 'Data Visualization View' + ); const geocoderRef = useRef(null); const [searchedProperty, setSearchedProperty] = useState({ coordinates: [-75.1628565788269, 39.97008211622267], @@ -199,8 +202,8 @@ const PropertyMap: FC = ({ handleMapClick(e.lngLat); }; - const handleStyleChange = () => { - setCurrentStyle((prevStyle) => (prevStyle + 1) % mapStyles.length); + const handleStyleChange = (styleName: string) => { + setCurrentStyle(styleName); }; const moveMap = (targetPoint: LngLatLike) => { @@ -491,14 +494,13 @@ const PropertyMap: FC = ({ console.warn('Layers not found, skipping filter application.'); } }); - // map load return (
changeCursor(e, 'pointer')} onMouseLeave={(e) => changeCursor(e, 'default')} onClick={onMapClick} @@ -506,7 +508,12 @@ const PropertyMap: FC = ({ maxZoom={MAX_MAP_ZOOM} interactiveLayerIds={layers} onError={(e) => { - setHasLoadingError(true); + console.log(e); + if ( + e.message === + "The layer 'vacant_properties_tiles_polygons' does not exist in the map's style and cannot be queried for features." + ) + setHasLoadingError(true); }} onLoad={(e) => { setMap(e.target); @@ -516,14 +523,7 @@ const PropertyMap: FC = ({ }} onMoveEnd={handleSetFeatures} > - - + {popupInfo && (