我对代码没有经验,这可能是我犯这个错误的原因——我不知道我在这里发布的代码是否正确,所以请原谅我的新手..
但无论如何我都在努力使这项工作成功。我有一个应用程序从 api 获取列表,返回并在网页上显示它们。有一张地图,我正在尝试向其添加多边形搜索(这样我就可以在地图上绘制以获取我的结果)但我无法让它工作,关于地图/javascript 需要数组中的数据,但是需要它的 api 调用(来自多边形的坐标)在一个字符串中。所以我不知道如何让这两部分一起工作。
每当我尝试将数据作为数组传递以便它显示在列表中时,我都会收到此错误,但随后 api(GET /propertyextendedsearch 'polygon' 参数)返回错误的格式错误。当我拥有现在的组件时,所有 api 调用都按预期工作,但多边形搜索不返回任何结果(没有显示我可以看到的错误,只是地图或列表上没有显示结果)
错误 polygonCoords.map 不是函数 TypeError: polygonCoords.map 不是函数 在
在处理FinishDrawing
在新班级。
下面是相关代码(部分与问题无关,仅供参考):
`//Listings jsx
import React, { useState, useEffect } from "react";
import Map from "../components/map/Map";
import SearchBar from "../components/searchBar/SearchBar";
import ListingList from "../components/listingsList/ListingList";
import LoadingSpinner from '../components/loading/LoadingSpinner';
import FilterBar from "../components/filterBar/FilterBar";
import L from "leaflet";
import leafletPip from "leaflet-pip";
const Listings = ({ fetchListings, loading, filters, query, setFilters }) => {
const [filteredData, setFilteredData] = useState([]);
const [polygonCoords, setPolygonCoords] = useState("");
const [searchType, setSearchType] = useState("");
const isListingInPolygon = (listing, coords) => {
if (!coords || !Array.isArray(coords) || coords.length === 0) return true;
const point = L.latLng(listing.latitude, listing.longitude);
const polygon = L.polygon(coords, { color: 'blue' });
const layerGroup = L.layerGroup().addLayer(polygon);
return leafletPip.pointInLayer(point, layerGroup).length > 0;
};
const defaultCenter = { lat: 45.5152, lng: -122.6784 };
const [searchedCenter, setSearchedCenter] = useState(defaultCenter);
useEffect(() => {
if (query === "" && searchType === "") return; // Add this line
const searchByPolygon = polygonCoords && polygonCoords.length > 0;
const fetchData = async () => {
if (searchByPolygon) {
const data = await fetchListings("", filters, polygonCoords, "polygon");
if (data && data.length > 0) {
const filteredListings = data.filter((listing) =>
isListingInPolygon(listing, polygonCoords)
);
setFilteredData(filteredListings);
} else {
setFilteredData([]);
}
} else {
const data = await fetchListings(query, filters, null, "location");
setFilteredData(data);
}
};
fetchData();
}, [filters, fetchListings, query, polygonCoords]);
const handleFiltersChange = (newFilters) => {
setFilters(newFilters);
};
return (
<div className="listings">
<SearchBar
fetchListings={(query) => fetchListings(query, filters)}
setSearchedCenter={setSearchedCenter}
defaultCenter={defaultCenter}
setSearchType={setSearchType} // Add this line
/>
<FilterBar
filters={filters}
onFiltersChange={handleFiltersChange}
fetchListings={fetchListings}
query={query}
/>
<div className="listingsPage">
<Map
center={searchedCenter}
zoom={13}
listings={filteredData}
setPolygonCoords={setPolygonCoords}
setSearchType={setSearchType}
fetchListings={fetchListings}
query={query}
searchType={searchType} // Add this line
/>
{loading ? (
<LoadingSpinner />
) : (
<ListingList listings={filteredData} />
)}
</div>
</div>
);
};
export default Listings;`
`//Map jsx (Leaflet Final))
import React, { useState, useEffect } from "react";
import InfoMarker from "../infoMarker/InfoMarker";
import ListingCard from "../listingCard/ListingCard";
import { MapContainer, TileLayer, Marker as LeafletMarker, Popup, useMap } from "react-leaflet";
import "leaflet/dist/leaflet.css";
import PolygonHandler from "./PolygonHandler";
import "@geoman-io/leaflet-geoman-free";
import "@geoman-io/leaflet-geoman-free/dist/leaflet-geoman.css";
import L from "leaflet";
import "./Map.css";
function ChangeView({ center, zoom }) {
const map = useMap();
useEffect(() => {
map.setView(center, zoom);
}, [center, zoom, map]);
return null;
}
const Map = ({
center,
zoom,
listings,
polygonCoords,
setPolygonCoords,
setSearchType,
fetchListings,
filters,
searchType,
}) => {
const [centerObj, setCenter] = useState(center);
const [selected, setSelected] = useState(null);
const [openModalZpid, setOpenModalZpid] = useState(null);
const openModal = (zpid) => {
setOpenModalZpid(zpid);
};
const handleClick = () => {
setSelected(null);
};
useEffect(() => {
if (listings && listings.length > 0) {
const firstValidListing = listings.find((listing) => listing.latitude && listing.longitude);
if (firstValidListing) {
const obj = { lat: firstValidListing.latitude, lng: firstValidListing.longitude };
setCenter(obj);
} else if (searchType !== "polygon") {
setCenter(center);
}
} else if (searchType !== "polygon") {
setCenter(center);
}
}, [listings, center, searchType])
const handleFinishDrawing = (polygonCoordsStr) => {
console.log("Polygon coordinates string:", polygonCoordsStr);
if (!polygonCoordsStr) {
console.error("Polygon coordinates string is undefined");
return;
}
const polygonCoords = polygonCoordsStr
.split(",")
.map((coordStr) => {
const [lng, lat] = coordStr.trim().split(" ");
return L.latLng(parseFloat(lat), parseFloat(lng));
});
// Add the first coordinate to the end of the array to close the polygon
polygonCoords.push(polygonCoords[0]);
// Format the polygon coordinates into a string for the API
const formattedPolygonCoords = polygonCoords
.map((coord) => `${coord.lng}%20${coord.lat}`)
.join("%2C");
console.log("Polygon coordinates:", polygonCoords);
setSearchType("polygon");
// Remove 'polygon=' from the parameter string
const polygonParam = formattedPolygonCoords;
console.log("Polygon parameter being sent to the API:", polygonParam); // Log the final string
fetchListings("", filters, polygonCoords, "polygon");
console.log("Finished drawing polygon");
};
return (
<>
{selected &&
<InfoMarker info={selected} handleClick={handleClick} openModal={setOpenModalZpid} />
}
<div className="leafletMapReact" style={{ height: "85vh", width: "50%" }}>
<MapContainer center={centerObj} zoom={zoom} style={{ height: "85vh", maxWidth: "100%", maxHeight: "100%" }}>
<ChangeView center={centerObj} zoom={zoom} />
<TileLayer
url="https://tile.jawg.io/9357cba3-7e9d-4a74-ad30-1c2a39c27360/{z}/{x}/{y}{r}.png?access-token=VT0F6yZuMFa7Mnk9psknNdbaX994oc629Rd7cA4B8Bg1Jf5H1l2FQtvBrG5GFq0F"
attribution='<a href="https://www.jawg.io" target="_blank">© Jawg</a> - <a href="https://www.openstreetmap.org" target="_blank">© OpenStreetMap</a> contributors'
/>
{listings &&
listings
.filter((listing) => listing.latitude && listing.longitude)
.map((listing) => (
<LeafletMarker
key={listing.zpid}
position={[listing.latitude, listing.longitude]}
eventHandlers={{
click: () => {
setSelected({
id: listing.zpid,
price: listing.price,
img: listing.imgSrc,
bed: listing.bedrooms,
bath: listing.bathrooms,
sqft: listing.livingArea,
address: listing.address,
});
},
}}
/>
))}
<PolygonHandler
setPolygonCoords={setPolygonCoords}
onFinishDrawing={handleFinishDrawing}
setSearchType={setSearchType}
/>
</MapContainer>
</div>
</>
);
};
Map.defaultProps = {
center: {
lat: 45.523064,
lng: -122.676483,
},
zoom: 11,
};
export default Map`
`// App.js
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import axios from "axios";
import Home from "./routes/Home";
import Navbar from "./components/navbar/NavBar";
import Listings from "./routes/Listings";
import Footer from "./components/footer/Footer";
import LoadingSpinner from "./components/loading/LoadingSpinner";
import React, { useState, useCallback, Suspense } from "react";
import axiosRateLimit from "axios-rate-limit";
const api = axiosRateLimit(axios.create(), { maxRequests: 1, perMilliseconds: 1000 }); // set the maximum number of API requests to 1 per second
function App() {
const [listings, setListings] = useState([]);
const [loading, setLoading] = useState(false);
const [filters, setFilters] = useState({
minPrice: "",
maxPrice: "",
bedsMin: "",
bedsMax: "",
bathsMin: "",
bathsMax: "",
sqftMin: "",
sqftMax: "",
buildYearMin: "",
buildYearMax: "",
lotSizeMin: "",
lotSizeMax: "",
daysOn: "",
status_type: "",
home_type: "",
sort: "",
});
const [searchType, setSearchType] = useState("");
const [query, setQuery] = useState("");
const length = listings?.length;
console.log(length + " this is the length");
const host = "zillow-com1.p.rapidapi.com";
const key = process.env.REACT_APP_API_BLUEFIN_ZILLOW_API_KEY;
const [polygonCoords, setPolygonCoords] = useState(null);
const fetchListings = useCallback(
(searchQuery, filtersToUpdate = filters, polygonCoords, searchType) => {
if (!searchQuery && !polygonCoords && searchType === "") return;
setQuery(searchQuery);
setLoading(true);
setSearchType(searchType);
const {
minPrice,
maxPrice,
bedsMin,
bedsMax,
bathsMin,
bathsMax,
sqftMin,
sqftMax,
buildYearMin,
buildYearMax,
lotSizeMin,
lotSizeMax,
daysOn,
status_type,
home_type,
sort,
} = filtersToUpdate;
let url = "";
url = `https://zillow-com1.p.rapidapi.com/propertyExtendedSearch?`;
if (polygonCoords) {
const formattedPolygonCoords = polygonCoords
.map((coord) => `${coord.lng}%20${coord.lat}`)
.join("%2C");
url += `&polygon=${formattedPolygonCoords}`;
} else if (typeof searchQuery === "string") {
url += `&location=${searchQuery}`;
}
if (minPrice) {
url += `&minPrice=${minPrice}`;
}
if (maxPrice) {
url += `&maxPrice=${maxPrice}`;
}
if (bedsMin) {
url += `&bedsMin=${bedsMin}`;
}
if (bedsMax) {
url += `&bedsMax=${bedsMax}`;
}
if (bathsMin) {
url += `&bathsMin=${bathsMin}`;
}
if (bathsMax) {
url += `&bathsMax=${bathsMax}`;
}
if (sqftMin) {
url += `&sqftMin=${sqftMin}`;
}
if (sqftMax) {
url += `&sqftMax=${sqftMax}`;
}
if (buildYearMin) {
url += `&buildYearMin=${buildYearMin}`;
}
if (buildYearMax) {
url += `&buildYearMax=${buildYearMax}`;
}
if (lotSizeMin) {
url += `&lotSizeMin=${lotSizeMin}`;
}
if (lotSizeMax) {
url += `&lotSizeMax=${lotSizeMax}`;
}
if (daysOn) {
url += `&daysOn=${daysOn}`;
}
if (status_type) {
url += `&status_type=${status_type}`;
}
if (home_type) {
url += `&home_type=${home_type}`;
}
if (sort) {
url += `&sort=${sort}`;
}
const options = {
method: 'GET',
url,
headers: {
'x-rapidapi-key': key,
'x-rapidapi-host': host,
},
};
return api
.request(options)
.then((response) => {
let props = response.data.props;
if (props?.length === 0) {
setListings([]);
setLoading(false);
return [];
}
setListings(props);
setLoading(false);
return props;
})
.catch((error) => {
console.error(error);
setListings([]);
setLoading(false);
return [];
});
},
[filters, key]
);
const handleClearFilters = useCallback(() => {
setFilters({
minPrice: "",
maxPrice: "",
bedsMin: "",
bedsMax: "",
bathsMin: "",
bathsMax: "",
sqftMin: "",
sqftMax: "",
buildYearMin: "",
buildYearMax: "",
lotSizeMin: "",
lotSizeMax: "",
daysOn: "",
status_type: "",
home_type: "",
sort: "",
});
}, []);
return (
<Suspense fallback={<div>Loading...</div>}>
<Navbar />
{loading && <LoadingSpinner />}
<Router>
<Routes>
<Route
exact
path="/"
element={
<Home
fetchListings={fetchListings}
listings={listings}
loading={loading}
/>
}
/>
<Route
path="/listings"
element={
<Listings
fetchListings={fetchListings}
listings={listings}
loading={loading}
filters={filters}
query={query}
setFilters={setFilters}
handleClearFilters={handleClearFilters}
polygonCoords={polygonCoords}
setPolygonCoords={setPolygonCoords}
searchType={searchType}
setSearchType={setSearchType}
/>
}
/>
</Routes>
</Router>
<Footer />
</Suspense>
);
}
export default App;`
`// SearchBar jsx
import React, { useState } from "react";
import { AiOutlineSearch } from "react-icons/ai";
import "./SearchBar.css";
const SearchBar = ({ fetchListings, setSearchType, }) => {
const [query, setQuery] = useState("");
const onSubmit = (e) => {
e.preventDefault();
setSearchType("");
fetchListings(query);
};
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<div className="searchBar">
<form className="sBar" onSubmit={onSubmit}>
<input
value={query}
onChange={handleInputChange}
type="text"
placeholder=" City, Address, School, Agent, ZIP"
/>
<button type="submit">
<AiOutlineSearch className="icon" />
</button>
</form>
</div>
);
};
export default SearchBar;
//PolygonHandler.jsx
...
if (allowDrawing) {
options.drawPolygon = true;
}
map.pm.addControls(options);
map.pm.setGlobalOptions({
edit: {
snappable: true,
snapDistance: 20,
},
});
if (allowDrawing) {
map.pm.enableDraw("");
}
map.on("pm:create", (event) => {
const coords = event.layer
.getLatLngs()[0]
.map((latLng) => `${latLng.lng} ${latLng.lat}`)
.reverse()
.join(",");
setPolygonCoords(coords);
if (onFinishDrawing) {
onFinishDrawing(coords);
}
});
return () => {
map.pm.removeControls();
map.off("pm:create");
}; },[map, setPolygonCoords, allowDrawing, onFinishDrawing]);
return null;
};
export default PolygonHandler;
`
`
// SearchBar jsx
import React, { useState } from "react";
import { AiOutlineSearch } from "react-icons/ai";
import "./SearchBar.css";
const SearchBar = ({ fetchListings, setSearchType, }) => {
const [query, setQuery] = useState("");
const onSubmit = (e) => {
e.preventDefault();
setSearchType("");
fetchListings(query);
};
const handleInputChange = (event) => {
setQuery(event.target.value);
};
return (
<div className="searchBar">
<form className="sBar" onSubmit={onSubmit}>
<input
value={query}
onChange={handleInputChange}
type="text"
placeholder=" City, Address, School, Agent, ZIP"
/>
<button type="submit">
<AiOutlineSearch className="icon" />
</button>
</form>
</div>
);
};
export default SearchBar;
`
//ListingList jsx
import React from 'react'
import ListingCard from '../listingCard/ListingCard'
import "./ListingList.css"
const ListingList = ({ listings }) => {
return (
<div className='listingListSection ml-1'>
{listings && listings.map((listing) => (
<ListingCard key={listing.zpid} listing={listing} />
))}
</div>
)
}
export default ListingList;
`
`//InfoMarker jsx
import React from "react";
import { Modal, Card, Button } from "flowbite-react";
import { RiCloseFill } from "react-icons/ri";
import L from "leaflet";
import "beautifymarker/leaflet-beautify-marker-icon.css";
import "beautifymarker";
import "./InfoMarker.css";
const InfoMarker = ({ info, handleClick, openModal, setSelected }) => {
const icon = L.BeautifyIcon.icon({
icon: "home",
iconShape: "marker",
borderColor: "#f00",
textColor: "#f00",
});
return (
<Modal id="infoMarkerModal" show className="max-w-[75vmin] h-auto">
<div className="popupContainer max-w-[80vmin] h-auto">
<div>
<Card imgSrc={info.img} className="h-full w-full" />
</div>
<div className="popupRight flex flex-col justify-between p-4 w-full">
<div>
<h5 className="text-2xl font-bold text-gray-900 dark:text-white">${info.price.toLocaleString()}</h5>
<p className="font-bold text-gray-900 dark:text-white">{info.address}</p>
<p className="text-gray-700 dark:text-gray-400">{info.bed} Beds | {info.bath} Baths | {info.sqft} Sqft </p>
</div>
<div className="flex justify-end">
<Button onClick={handleClick} className="mr-2">
Close
</Button>
<Button
onClick={() => openModal(info.id)}
className="bg-blue-600 hover:bg-blue-700 text-white"
>
More Details
</Button>
</div>
</div>
<div className="closeIcon absolute right-0 top-0 mt-2 mr-2">
<RiCloseFill
onClick={handleClick}
className="text-white text-2xl cursor-pointer"
/>
</div>
</div>
<L.Marker
position={[info.latitude, info.longitude]}
icon={icon}
eventHandlers={{
click: () => {
setSelected({
id: info.zpid,
price: info.price,
img: info.imgSrc,
bed: info.bedrooms,
bath: info.bathrooms,
sqft: info.livingArea,
address: info.address,
});
},
}}
/>
</Modal>
);
};
export default InfoMarker;
`