diff --git a/.firebase/hosting.YnVpbGQ.cache b/.firebase/hosting.YnVpbGQ.cache index 6cfaf2d..f228ddf 100644 --- a/.firebase/hosting.YnVpbGQ.cache +++ b/.firebase/hosting.YnVpbGQ.cache @@ -1,15 +1,15 @@ robots.txt,1725691795409,b2090cf9761ef60aa06e4fab97679bd43dfa5e5df073701ead5879d7c68f1ec5 manifest.json,1725691795408,4368aeaf848ae2e048765562c289452f33ad2a175c4b1951ea8bdf2ada0d5b10 -index.html,1725749057871,effc881b82c9d3c89ee011b5ffb8dafb953803f2ac2de53235fe47d2f9983b74 logo512.png,1725691795407,212b102aa09e51b3b3e06647e81f7801a61333e171f6582e8124379aabccb41d logo192.png,1725691795406,79e2b749561016bc8af300ea19f48347ceed3cb1a54f48ae456172eca45e08f0 -asset-manifest.json,1725749057871,a8269c53ccbff485f0c05c52cc060c9db9fb8241b26989acfeee1e5138bdfa55 favicon.ico,1725691795404,27edce7be5922cf0bef7d4136f69b5bfbdd5bf8c13c7b026f71187d41a00aa7d -static/js/main.db631921.js.LICENSE.txt,1725749057877,92ec6870e0d50ef16c11a5af58e8bae63fc674a797281dae6198d723b4582cff -static/js/453.ed3810f9.chunk.js.map,1725749057877,b580f924df88443006fe2c2050df639443c362a4697c20b3a6bd730b8bbd83b0 -static/js/453.ed3810f9.chunk.js,1725749057877,ad49a5ec5043145417a4fe58d70d740daa4764cb309e9bb4bcc93dfbdf2b5643 -static/css/main.79d19d76.css,1725749057877,8532eb1bc08eefe39074a437fdd2823b7e14851191c7cd07f96140abf432a5b4 -static/css/main.79d19d76.css.map,1725749057877,863af17a469ff836e46a378ba7cf2a08b3d4a827991c0be88c37d888414ad7c9 -static/media/poster.0fad7cc42115f255a281.png,1725749057877,f3025012e905a2fd748b83ff036dc7fd5dbc6b6aa6493435a0c7a60db866b5bf -static/js/main.db631921.js,1725749057878,8665bc87d6ae3290e1fcd49e18c2c356b1bafd522c781980eb1048fd9ed4f032 -static/js/main.db631921.js.map,1725749057878,906088b6f783d5ea680d84a43f521330163825d2a8fa738b7546888a52ab1dfd +index.html,1725753484979,153961d4989c6d46bcc5c97b2e9c10d3790e35a5d888ceb86d3098fcdb5b430c +asset-manifest.json,1725753484979,22c4614a5b692c852c53d856a32c2abffa3a164ccde683a3ba3351f9b4202082 +static/js/main.ec691045.js.LICENSE.txt,1725753484993,30f88fcb7f8d73132245dec8d321969add3035e37dbd531f61107e638f8efae4 +static/js/453.ed3810f9.chunk.js,1725753484993,ad49a5ec5043145417a4fe58d70d740daa4764cb309e9bb4bcc93dfbdf2b5643 +static/js/453.ed3810f9.chunk.js.map,1725753484995,b580f924df88443006fe2c2050df639443c362a4697c20b3a6bd730b8bbd83b0 +static/css/main.79d19d76.css,1725753484993,8532eb1bc08eefe39074a437fdd2823b7e14851191c7cd07f96140abf432a5b4 +static/css/main.79d19d76.css.map,1725753484995,863af17a469ff836e46a378ba7cf2a08b3d4a827991c0be88c37d888414ad7c9 +static/media/poster.0fad7cc42115f255a281.png,1725753484993,f3025012e905a2fd748b83ff036dc7fd5dbc6b6aa6493435a0c7a60db866b5bf +static/js/main.ec691045.js,1725753484995,849f70ba603fde8c7486961ea3fb0ffc350c24a351254ff97759f3b3900e6d41 +static/js/main.ec691045.js.map,1725753484995,88483bea13905e9d56d917ffdbb246fd33d6bc708e8f0e9322b38a2046e6fb4a diff --git a/package-lock.json b/package-lock.json index b6b456c..e9e444f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "my-app", "version": "0.1.0", "dependencies": { + "@react-google-maps/api": "^2.19.3", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -3044,6 +3045,23 @@ "resolved": "https://registry.npmjs.org/@firebase/webchannel-wrapper/-/webchannel-wrapper-1.0.1.tgz", "integrity": "sha512-jmEnr/pk0yVkA7mIlHNnxCi+wWzOFUg0WyIotgkKAb2u1J7fAeDBcVNSTjTihbAYNusCLQdW5s9IJ5qwnEufcQ==" }, + "node_modules/@googlemaps/js-api-loader": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/@googlemaps/js-api-loader/-/js-api-loader-1.16.2.tgz", + "integrity": "sha512-psGw5u0QM6humao48Hn4lrChOM2/rA43ZCm3tKK9qQsEj1/VzqkCqnvGfEOshDbBQflydfaRovbKwZMF4AyqbA==", + "dependencies": { + "fast-deep-equal": "^3.1.3" + } + }, + "node_modules/@googlemaps/markerclusterer": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/@googlemaps/markerclusterer/-/markerclusterer-2.5.3.tgz", + "integrity": "sha512-x7lX0R5yYOoiNectr10wLgCBasNcXFHiADIBdmn7jQllF2B5ENQw5XtZK+hIw4xnV0Df0xhN4LN98XqA5jaiOw==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "supercluster": "^8.0.1" + } + }, "node_modules/@grpc/grpc-js": { "version": "1.9.15", "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.9.15.tgz", @@ -4178,6 +4196,33 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, + "node_modules/@react-google-maps/api": { + "version": "2.19.3", + "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.19.3.tgz", + "integrity": "sha512-jiLqvuOt5lOowkLeq7d077AByTyJp+s6hZVlLhlq7SBacBD37aUNpXBz2OsazfeR6Aw4a+9RRhAEjEFvrR1f5A==", + "dependencies": { + "@googlemaps/js-api-loader": "1.16.2", + "@googlemaps/markerclusterer": "2.5.3", + "@react-google-maps/infobox": "2.19.2", + "@react-google-maps/marker-clusterer": "2.19.2", + "@types/google.maps": "3.55.2", + "invariant": "2.2.4" + }, + "peerDependencies": { + "react": "^16.8 || ^17 || ^18", + "react-dom": "^16.8 || ^17 || ^18" + } + }, + "node_modules/@react-google-maps/infobox": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/@react-google-maps/infobox/-/infobox-2.19.2.tgz", + "integrity": "sha512-6wvBqeJsQ/eFSvoxg+9VoncQvNoVCdmxzxRpLvmjPD+nNC6mHM0vJH1xSqaKijkMrfLJT0nfkTGpovrF896jwg==" + }, + "node_modules/@react-google-maps/marker-clusterer": { + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/@react-google-maps/marker-clusterer/-/marker-clusterer-2.19.2.tgz", + "integrity": "sha512-x9ibmsP0ZVqzyCo1Pitbw+4b6iEXRw/r1TCy3vOUR3eKrzWLnHYZMR325BkZW2r8fnuWE/V3Fp4QZOP9qYORCw==" + }, "node_modules/@remix-run/router": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.19.1.tgz", @@ -4989,6 +5034,11 @@ "@types/send": "*" } }, + "node_modules/@types/google.maps": { + "version": "3.55.2", + "resolved": "https://registry.npmjs.org/@types/google.maps/-/google.maps-3.55.2.tgz", + "integrity": "sha512-JcTwzkxskR8DN/nnX96Pie3gGN3WHiPpuxzuQ9z3516o1bB243d8w8DHUJ8BohuzoT1o3HUFta2ns/mkZC8KRw==" + }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -10982,6 +11032,14 @@ "node": ">= 0.4" } }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", @@ -13949,6 +14007,11 @@ "node": ">=4.0" } }, + "node_modules/kdbush": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/kdbush/-/kdbush-4.0.2.tgz", + "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" + }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -18610,6 +18673,14 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/supercluster": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/supercluster/-/supercluster-8.0.1.tgz", + "integrity": "sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==", + "dependencies": { + "kdbush": "^4.0.2" + } + }, "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", diff --git a/package.json b/package.json index 55f951d..0a27b74 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "dependencies": { + "@react-google-maps/api": "^2.19.3", "@testing-library/jest-dom": "^5.17.0", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", diff --git a/src/App.js b/src/App.js index f7a82db..3370dab 100644 --- a/src/App.js +++ b/src/App.js @@ -12,6 +12,9 @@ import Gallery from './components/Gallery.js'; import MapComponent from './components/ArtMap.js'; import DetailComponent from './components/ArtDetail.js'; +import ArtInfo from './components/record.js'; +import Component from './components/GetLocation.js'; + //////////////// json data ////////////////// import artDataJson from './data/artData.json'; @@ -37,12 +40,13 @@ function App() { {/* } /> */} {/* 我的地點頁面 */} - {/* } - /> */} - + element={} + /> + + ); diff --git a/src/components/ArtDetail.js b/src/components/ArtDetail.js index 20eb136..f46aa84 100644 --- a/src/components/ArtDetail.js +++ b/src/components/ArtDetail.js @@ -1,8 +1,7 @@ import React, { useEffect, useState } from "react"; import { useParams, Link } from "react-router-dom"; - import artDataJson from '../data/artData.json'; - +import { getCurrentLocation } from './GetLocation'; // 匯入位置取得函數 // Haversine formula to calculate distance between two lat/lon points const haversineDistance = (coords1, coords2) => { @@ -25,30 +24,63 @@ const DetailComponent = () => { const { artId } = useParams(); // Get artId from route parameters const [artPiece, setArtPiece] = useState(null); const [distance, setDistance] = useState(null); - - // 假設我們在台北市中心(25.0330, 121.5654) - const currentLocation = { - latitude: 25.0330, - longitude: 121.5654 - }; + const [currentLocation, setCurrentLocation] = useState(null); // 使用者位置狀態 + const [error, setError] = useState(null); + + // 用於不斷嘗試取得位置資料,直到成功或達到最大次數 + useEffect(() => { + let intervalId; + let maxAttempts = 10; // 最大嘗試次數 + let attemptCount = 0; + + const fetchLocation = () => { + getCurrentLocation() + .then((location) => { + setCurrentLocation(location); + clearInterval(intervalId); // 成功取得位置後清除定時器 + }) + .catch((err) => { + console.error(err); + attemptCount++; + if (attemptCount >= maxAttempts) { + setError("無法取得您的位置,請啟用定位服務或稍後重試"); + clearInterval(intervalId); // 達到最大次數後停止嘗試 + } + }); + }; + + // 每2秒嘗試取得一次位置 + intervalId = setInterval(fetchLocation, 2000); + // 在組件卸載時清除定時器 + return () => clearInterval(intervalId); + }, []); + + // 根據使用者位置和藝術品位置計算距離 useEffect(() => { + // Find the art piece in local JSON data const piece = artDataJson.find(p => p.系統編號 === artId); setArtPiece(piece); - if (piece) { - // Calculate the distance + if (piece && currentLocation) { + // 確保緯度和經度是數字 const artLocation = { - latitude: parseFloat(piece.緯度), - longitude: parseFloat(piece.經度) + latitude: Number(piece.緯度), // 使用 Number() 來轉換為數字 + longitude: Number(piece.經度) }; + + currentLocation.latitude = Number(currentLocation.lat); + currentLocation.longitude = Number(currentLocation.lng); + + // 計算距離 const calculatedDistance = haversineDistance(currentLocation, artLocation).toFixed(1); - setDistance(calculatedDistance); + setDistance(calculatedDistance) } - }, [artId]); + }, [artId, currentLocation]); // 依賴 currentLocation 來重新計算距離 - if (!artPiece) return

加載中...

; + // 檢查 currentLocation 是否存在,否則不渲染 + if (!artPiece || !currentLocation) return

加載中...

; const isCheckInEnabled = distance && distance < 50; @@ -56,13 +88,13 @@ const DetailComponent = () => {
-

{artPiece.作品名稱}

+

{artPiece.作品名稱}

作者: {artPiece.作者}

設置地點: {artPiece.設置地點}

場域: {artPiece.場域}

{artPiece.作品說明}

{artPiece.作品名稱} - + {distance && (

距離當前位置: {distance < 1000 ? `${distance} 公尺` : `${(distance / 1000).toFixed(2)} 公里`} @@ -78,8 +110,9 @@ const DetailComponent = () => { {isCheckInEnabled ? "打卡" : `距離需小於50公尺,當前距離: ${distance} 公尺`} -

+ + {error &&

{error}

}
diff --git a/src/components/Gallery.js b/src/components/Gallery.js index dc37bd2..6ad586d 100644 --- a/src/components/Gallery.js +++ b/src/components/Gallery.js @@ -4,13 +4,13 @@ import { Link } from 'react-router-dom'; // Data import artDataJson from '../data/artData.json'; //import './css/artlist.css'; - +import { getCurrentLocation } from './GetLocation'; // 匯入位置取得函數 // Haversine formula to calculate the distance between two points const haversineDistance = (coords1, coords2) => { - const toRad = (x) => (x * Math.PI) / 180; - const R = 6371; // Radius of the Earth in km + const toRad = (x) => (x * Math.PI) / 180; // 角度轉換為弧度 + const R = 6371; // 地球半徑 (公里) const dLat = toRad(coords2.latitude - coords1.latitude); const dLon = toRad(coords2.longitude - coords1.longitude); const lat1 = toRad(coords1.latitude); @@ -20,46 +20,59 @@ const haversineDistance = (coords1, coords2) => { Math.sin(dLat / 2) * Math.sin(dLat / 2) + Math.sin(dLon / 2) * Math.sin(dLon / 2) * Math.cos(lat1) * Math.cos(lat2); const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - const distance = R * c * 1000; + const distance = R * c * 1000; // 將距離轉換為公尺 return distance; }; -const GalleryPage = () => { - // Load data into state (in a real case, you would fetch this from an API) - const [artData, setArtData] = useState([]); - // 假設我們在台北市中心(25.0330, 121.5654) - const currentLocation = { - latitude: 25.0330, - longitude: 121.5654 - }; +const GalleryPage = () => { + const [artData, setArtData] = useState([]); // 藝術品資料狀態 + const [currentLocation, setCurrentLocation] = useState(null); // 使用者位置狀態 useEffect(() => { - // Load art data + // 加載藝術品資料 setArtData(artDataJson); - }, []); + + // 使用 getCurrentLocation 動態取得使用者位置 + getCurrentLocation() + .then(location => setCurrentLocation({ + latitude: location.lat, + longitude: location.lng + })) + .catch(error => { + console.error("無法取得位置", error); + setCurrentLocation({ + latitude: 25.0330, // 預設為台北市中心 + longitude: 121.5654 + }); + }); + }, []); // 空陣列作為依賴,表示這個 useEffect 僅在組件初次加載時運行 - // Calculate distances and sort the art list + if (!currentLocation) { + return

正在加載位置...

; + } + + // 計算每個藝術品與使用者的距離,並排序 const sortedArtData = artData.map((item) => { const artLocation = { - latitude: parseFloat(item.緯度), // Convert string to number - longitude: parseFloat(item.經度), // Convert string to number + latitude: parseFloat(item.緯度), + longitude: parseFloat(item.經度), }; - const distance = haversineDistance(currentLocation, artLocation).toFixed(1); // Calculate distance + const distance = haversineDistance(currentLocation, artLocation).toFixed(1); // 計算距離 return { ...item, distance }; }); - - // Sort by distance + + // 依距離進行排序 sortedArtData.sort((a, b) => a.distance - b.distance); return ( - {/* Bottom Navigation */} + + {/* 底部導航按鈕 */}
- + 回地圖
diff --git a/src/components/GetLocation.js b/src/components/GetLocation.js new file mode 100644 index 0000000..d02d48e --- /dev/null +++ b/src/components/GetLocation.js @@ -0,0 +1,22 @@ +// GetLocation.js +export const getCurrentLocation = () => { + return new Promise((resolve, reject) => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + (position) => { + const location = { + lat: position.coords.latitude, + lng: position.coords.longitude, + }; + resolve(location); + }, + (error) => { + reject(error); + } + ); + } else { + reject(new Error('Geolocation is not supported by this browser.')); + } + }); + }; + \ No newline at end of file diff --git a/src/components/record.js b/src/components/record.js index 1bb84c8..9f3c479 100644 --- a/src/components/record.js +++ b/src/components/record.js @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react'; -import artData from './artData.json'; // 將本地 artData.json 檔案匯入 +import artData from '../data/artData.json'; // 將本地 artData.json 檔案匯入 //import './css/record.css';