diff --git a/.github/workflows/pull-requests.yml b/.github/workflows/pull-requests.yml index 9749588..7986d58 100644 --- a/.github/workflows/pull-requests.yml +++ b/.github/workflows/pull-requests.yml @@ -13,7 +13,7 @@ jobs: uses: actions/checkout@v4 - name: Use Node.js - uses: actions/setup-node@v3 + uses: actions/setup-node@v4 with: node-version: '16' diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index be35258..ea03092 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -24,13 +24,13 @@ jobs: - name: Checkout uses: actions/checkout@v4 - name: Setup Pages - uses: actions/configure-pages@v3 + uses: actions/configure-pages@v4 - name: Build run: make build && make pages - name: Upload artifact - uses: actions/upload-pages-artifact@v2 + uses: actions/upload-pages-artifact@v3 with: path: 'pages' - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v2 + uses: actions/deploy-pages@v4 diff --git a/assets/js/main.js b/assets/js/main.js index 735bf52..4847fb5 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -1,178 +1,252 @@ -let isProcessingCSV = false; -let isProcessingKML = false; - -const fetchMeetings = (query, callback, isCSV, isKML) => { - const script = document.createElement("script"); - script.src = `${query}&callback=${callback.name}`; - document.body.appendChild(script); - isProcessingCSV = isCSV; - isProcessingKML = isKML; -}; - -const handleMeetingsData = (meetings) => { - if (isProcessingCSV) { +class MeetingDataProcessor { + constructor() { + this.isProcessingCSV = false; + this.isProcessingKML = false; + } + + // Load JSONP data + fetchMeetings(query, isCSV, isKML) { + MeetingDataProcessor.clearError(); + MeetingDataProcessor.hideLinks(); + return new Promise((resolve, reject) => { + const script = document.createElement("script"); + const callbackName = `jsonpCallback_${Date.now()}`; + + window[callbackName] = (data) => { + document.body.removeChild(script); + delete window[callbackName]; + + // check for empty array + if (Array.isArray(data) && data.length === 0) { + const errorMsg = "No data found"; + MeetingDataProcessor.displayError(errorMsg); + reject(new Error(errorMsg)); + } else { + this.handleMeetingsData(data, isCSV, isKML); + resolve(data); + } + }; + + script.src = `${query}&callback=${callbackName}`; + script.onerror = () => { + const errorMsg = "Error loading data"; + MeetingDataProcessor.displayError(errorMsg); + reject(new Error(errorMsg)); + }; + document.body.appendChild(script); + + this.isProcessingCSV = isCSV; + this.isProcessingKML = isKML; + }); + } + + static displayError(message) { + const errorContainer = document.getElementById("errorMessages"); + if (errorContainer) { + errorContainer.textContent = message; + errorContainer.style.display = "block"; + } else { + console.error("Error container not found in the document."); + } + } + + static clearError() { + const errorContainer = document.getElementById("errorMessages"); + if (errorContainer) { + errorContainer.style.display = "none"; + errorContainer.textContent = ""; + } else { + console.error("Error container not found in the document."); + } + } + + // Handle data once it's fetched + handleMeetingsData(meetings, isCSV, isKML) { + if (isCSV) { + this.exportCSV(meetings); + } + if (isKML) { + this.exportKML(meetings); + } + } + + // CSV export functionality + exportCSV(meetings) { const csvContent = `data:text/csv;charset=utf-8,${encodeURIComponent( - convertToCSV(meetings) + this.constructor.convertToCSV(meetings) )}`; const downloadLink = document.getElementById("downloadLink"); downloadLink.href = csvContent; downloadLink.style.display = "block"; } - if (isProcessingKML) { + // KML export functionality + exportKML(meetings) { const kmlContent = `data:text/xml;charset=utf-8,${encodeURIComponent( - convertToKML(meetings) + this.constructor.convertToKML(meetings) )}`; const kmlDownloadLink = document.getElementById("kmlDownloadLink"); kmlDownloadLink.href = kmlContent; kmlDownloadLink.style.display = "block"; } -}; -const convertToCSV = (data) => { - const csvRows = []; - const keys = Object.keys(data[0]); - - // header row - csvRows.push(keys.join(",")); + static hideLinks() { + const downloadLink = document.getElementById("downloadLink"); + downloadLink.style.display = "none"; + const kmlDownloadLink = document.getElementById("kmlDownloadLink"); + kmlDownloadLink.style.display = "none"; + } - data.forEach((row) => { - const values = keys.map((key) => { - let value = row[key]; - if (typeof value === "string") { - // Escape double quotes and wrap in double quotes if it contains a comma - if (value.includes(",") || value.includes('"')) { + // Convert data to CSV + static convertToCSV(data) { + const csvRows = []; + const keys = Object.keys(data[0]); + csvRows.push(keys.join(",")); + + data.forEach((row) => { + const values = keys.map((key) => { + let value = row[key]; + if ( + typeof value === "string" && + (value.includes(",") || value.includes('"')) + ) { value = `"${value.replace(/"/g, '""')}"`; } - } - return value; + return value; + }); + csvRows.push(values.join(",")); }); - csvRows.push(values.join(",")); - }); - - return csvRows.join("\n"); -}; + return csvRows.join("\n"); + } -const convertToKML = (data) => { - const kmlHeader = ` - + // Convert data to KML + static convertToKML(data) { + let kmlContent = ` - - `; - - const kmlFooter = ` + `; + + const placemarks = data + .map((meeting) => { + const name = meeting["meeting_name"].trim() || "NA Meeting"; + const lng = parseFloat(meeting["longitude"]); + const lat = parseFloat(meeting["latitude"]); + if (!lng || !lat) return ""; + + const description = MeetingDataProcessor.prepareSimpleLine(meeting); + const address = MeetingDataProcessor.prepareSimpleLine(meeting, false); + + return ` + ${name} + ${address ? `
${address}
` : ""} + ${description ? `${description}` : ""} + + ${lng},${lat} + +
`; + }) + .join("\n"); + + kmlContent += + placemarks + + `
`; - const placemarks = data.map((meeting) => { - const name = meeting["meeting_name"].trim() || "NA Meeting"; - const lng = parseFloat(meeting["longitude"]); - const lat = parseFloat(meeting["latitude"]); - - if (lng || lat) { - const description = prepareSimpleLine(meeting); - const address = prepareSimpleLine(meeting, false); - - return ( - ` \n` + - ` ${name}\n` + - (address ? `
${address}
\n` : "") + - (description - ? ` ${description}\n` - : "") + - ` \n` + - ` ${lng},${lat}\n` + - ` \n` + - `
\n` - ); - } - }); + return kmlContent; + } - return kmlHeader + placemarks.join("\n") + kmlFooter; -}; + // KML and CSV data preparation + static prepareSimpleLine(meeting, withDate = true) { + const getLocationInfo = () => { + const locationInfo = []; + const addInfo = (property) => { + if (property in meeting) { + const value = meeting[property].trim(); + if (value) { + locationInfo.push(value); + } + } + }; + + addInfo("location_text"); + addInfo("location_street"); + addInfo("location_city_subsection"); + addInfo("location_municipality"); + addInfo("location_neighborhood"); + addInfo("location_province"); + addInfo("location_postal_code_1"); + addInfo("location_nation"); + addInfo("location_info"); + + return locationInfo.join(", "); + }; -const exportData = () => { - const query = document.getElementById("query").value; - if (!query.includes("/client_interface/jsonp")) { - alert("Invalid BMLT query URL, must use jsonp endpoint."); - return; - } - // Only support GetSearchResults for KML - const isKML = query.includes("GetSearchResults"); - fetchMeetings(query, handleMeetingsData, true, isKML); - if (isKML) { - fetchMeetings(query, handleMeetingsData, true, true); - } -}; - -const prepareSimpleLine = (meeting, withDate = true) => { - const getLocationInfo = () => { - const locationInfo = []; - const addInfo = (property) => { - if (property in meeting) { - const value = meeting[property].trim(); - if (value) { - locationInfo.push(value); + const getDateString = () => { + const weekday_strings = [ + "All", + "Sunday", + "Monday", + "Tuesday", + "Wednesday", + "Thursday", + "Friday", + "Saturday", + ]; + const weekday = parseInt(meeting["weekday_tinyint"].trim()); + const weekdayString = weekday_strings[weekday]; + + const startTime = `2000-01-01 ${meeting["start_time"]}`; + const time = new Date(startTime); + + if (weekdayString && withDate) { + let dateString = weekdayString; + + if (!isNaN(time)) { + dateString += `, ${time.toLocaleTimeString("en-US", { + hour: "numeric", + minute: "numeric", + hour12: true, + })}`; } + + return dateString; } + + return ""; }; - addInfo("location_text"); - addInfo("location_street"); - addInfo("location_city_subsection"); - addInfo("location_municipality"); - addInfo("location_neighborhood"); - addInfo("location_province"); - addInfo("location_postal_code_1"); - addInfo("location_nation"); - addInfo("location_info"); - - return locationInfo.join(", "); - }; - - const getDateString = () => { - const weekday_strings = [ - "All", - "Sunday", - "Monday", - "Tuesday", - "Wednesday", - "Thursday", - "Friday", - "Saturday", - ]; - const weekday = parseInt(meeting["weekday_tinyint"].trim()); - const weekdayString = weekday_strings[weekday]; - - const startTime = `2000-01-01 ${meeting["start_time"]}`; - const time = new Date(startTime); - - if (weekdayString && withDate) { - let dateString = weekdayString; - - if (!isNaN(time)) { - dateString += `, ${time.toLocaleTimeString("en-US", { - hour: "numeric", - minute: "numeric", - hour12: true, - })}`; - } + const locationInfo = getLocationInfo(); + const dateString = getDateString(); + if (withDate && dateString && locationInfo) { + return `${dateString}, ${locationInfo}`; + } else if (dateString) { return dateString; + } else { + return locationInfo; } + } - return ""; - }; - - const locationInfo = getLocationInfo(); - const dateString = getDateString(); - - if (withDate && dateString && locationInfo) { - return `${dateString}, ${locationInfo}`; - } else if (dateString) { - return dateString; - } else { - return locationInfo; + // start the export process + exportData(query) { + if (!query.includes("/client_interface/jsonp")) { + MeetingDataProcessor.displayError( + "Invalid BMLT query URL, must use jsonp endpoint." + ); + return; + } + const isCSV = true; + const isKML = query.includes("GetSearchResults"); + this.fetchMeetings(query, isCSV, isKML).catch((error) => + console.error("Error fetching meetings:", error) + ); } -}; +} + +// Triggers data export process, bound to button click event +function exportData() { + const query = document.getElementById("query").value; + const processor = new MeetingDataProcessor(); + processor.exportData(query); +} diff --git a/index.html b/index.html index 53db6fb..40ad5cb 100644 --- a/index.html +++ b/index.html @@ -15,6 +15,7 @@

BMLT Data Converter

+ diff --git a/package-lock.json b/package-lock.json index fcd66d8..aebc412 100644 --- a/package-lock.json +++ b/package-lock.json @@ -39,9 +39,9 @@ } }, "node_modules/@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.22.20", @@ -626,9 +626,9 @@ } }, "node_modules/flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "dependencies": { "flatted": "^3.2.9", @@ -636,7 +636,7 @@ "rimraf": "^3.0.2" }, "engines": { - "node": ">=12.0.0" + "node": "^10.12.0 || >=12.0.0" } }, "node_modules/flatted": { @@ -690,9 +690,9 @@ } }, "node_modules/globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "dependencies": { "type-fest": "^0.20.2" @@ -996,9 +996,9 @@ } }, "node_modules/punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true, "engines": { "node": ">=6" @@ -1050,9 +1050,9 @@ } }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -1292,9 +1292,9 @@ "dev": true }, "@babel/highlight": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.20.tgz", - "integrity": "sha512-dkdMCN3py0+ksCgYmGG8jKeGA/8Tk+gJwSYYlFGxG5lmhfKNoAy004YpLxpS1W2J8m/EK2Ew+yOs9pVRwO89mg==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.22.20", @@ -1745,9 +1745,9 @@ } }, "flat-cache": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.1.tgz", - "integrity": "sha512-/qM2b3LUIaIgviBQovTLvijfyOQXPtSRnRK26ksj2J7rzPIecePUIpJsZ4T02Qg+xiAEKIs5K8dsHEd+VaKa/Q==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", "dev": true, "requires": { "flatted": "^3.2.9", @@ -1797,9 +1797,9 @@ } }, "globals": { - "version": "13.23.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.23.0.tgz", - "integrity": "sha512-XAmF0RjlrjY23MA51q3HltdlGxUpXPvg0GioKiD9X6HD28iMjo2dKC8Vqwm7lne4GNr78+RHTfliktR6ZH09wA==", + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, "requires": { "type-fest": "^0.20.2" @@ -2034,9 +2034,9 @@ "dev": true }, "punycode": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", "dev": true }, "regexpp": { @@ -2067,9 +2067,9 @@ } }, "semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", + "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", "dev": true, "requires": { "lru-cache": "^6.0.0"