-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
10 changed files
with
381 additions
and
415 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
<script lang="ts"> | ||
import { writable } from 'svelte/store'; | ||
import { fetchData, exportCSV, exportKML } from './DataUtils'; | ||
let query = ''; // User input for data query | ||
const processing = writable(false); | ||
const errorMessage = writable(''); | ||
let csvDownloadUrl = ''; | ||
let kmlDownloadUrl = ''; | ||
async function handleExport() { | ||
processing.set(true); | ||
errorMessage.set(''); | ||
csvDownloadUrl = ''; | ||
kmlDownloadUrl = ''; | ||
try { | ||
const data = await fetchData(query); | ||
if (data && data.length > 0) { | ||
// Process data for CSV | ||
csvDownloadUrl = exportCSV(data); | ||
// Process data for KML | ||
kmlDownloadUrl = query.includes('GetSearchResults') ? exportKML(data) : ''; | ||
} else { | ||
throw new Error('No data available for export.'); | ||
} | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
errorMessage.set(error.message || 'Failed to export data.'); | ||
} else { | ||
errorMessage.set('Failed to export data.'); | ||
} | ||
} finally { | ||
processing.set(false); | ||
} | ||
} | ||
</script> | ||
|
||
<section> | ||
<div id="export-form"> | ||
<h1>BMLT Data Converter</h1> | ||
<div id="inner-box"> | ||
<input | ||
type="text" | ||
bind:value={query} | ||
on:keydown={(event) => event.key === 'Enter' && handleExport()} | ||
placeholder="BMLT URL query..." | ||
/> | ||
<button on:click={handleExport} disabled={$processing}>Generate Export Data</button> | ||
|
||
{#if $errorMessage} | ||
<p class="error" id="errorMessages">{$errorMessage}</p> | ||
{/if} | ||
|
||
{#if csvDownloadUrl} | ||
<a href={csvDownloadUrl} class="download-links" download="exported_data.csv">Download CSV</a | ||
><br /> | ||
{/if} | ||
{#if kmlDownloadUrl} | ||
<a href={kmlDownloadUrl} class="download-links" download="exported_data.kml">Download KML</a | ||
> | ||
{/if} | ||
<div id="description">Converts BMLT data from JSON to CSV or KML</div> | ||
</div> | ||
<div id="footer"> | ||
<a | ||
href="https://github.com/bmlt-enabled/bmlt-data-converter/issues" | ||
class="footer-link" | ||
target="_blank">Issues?</a | ||
> | ||
</div> | ||
</div> | ||
</section> | ||
|
||
<style> | ||
.error { | ||
color: red; | ||
} | ||
h1 { | ||
text-align: center; | ||
} | ||
#export-form { | ||
font-family: Arial, sans-serif; | ||
max-width: 800px; | ||
margin: 20px auto 0; | ||
padding: 1px 20px 20px; | ||
background-color: #f7f7f7; | ||
border: 1px solid #ccc; | ||
border-radius: 5px; | ||
box-sizing: border-box; | ||
} | ||
#errorMessages { | ||
color: red; | ||
text-align: center; | ||
padding-top: 15px; | ||
} | ||
input[type='text'] { | ||
width: 100%; | ||
padding: 10px; | ||
margin-bottom: 10px; | ||
border: 1px solid #ccc; | ||
border-radius: 3px; | ||
box-sizing: border-box; | ||
} | ||
button { | ||
background-color: #007bff; | ||
width: 100%; | ||
color: #fff; | ||
border: none; | ||
padding: 10px 20px; | ||
margin-bottom: 15px; | ||
border-radius: 3px; | ||
cursor: pointer; | ||
} | ||
button:hover { | ||
background-color: #0056b3; | ||
} | ||
.download-links { | ||
text-align: center; | ||
text-decoration: none; | ||
color: #007bff; | ||
} | ||
.footer-link { | ||
display: block; | ||
text-align: center; | ||
text-decoration: none; | ||
color: #007bff; | ||
} | ||
a:hover { | ||
text-decoration: underline; | ||
} | ||
#inner-box { | ||
padding: 20px; | ||
background-color: #fff; | ||
border: 1px solid #ddd; | ||
border-radius: 3px; | ||
box-sizing: border-box; | ||
text-align: center; | ||
margin-bottom: 20px; | ||
} | ||
#description { | ||
text-align: center; | ||
margin-top: 20px; | ||
font-size: 1em; | ||
color: #333; | ||
} | ||
#footer { | ||
padding: 10px; | ||
background-color: #e0d8d8; | ||
border: 1px solid #e0d8d8; | ||
border-radius: 3px; | ||
box-sizing: border-box; | ||
text-align: center; | ||
} | ||
</style> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,155 @@ | ||
import fetchJsonp from 'fetch-jsonp'; | ||
|
||
export async function fetchData(query: string): Promise<any[]> { | ||
try { | ||
if (!query.includes('/client_interface/json')) { | ||
throw new Error('Query does not contain a valid json endpoint.'); | ||
} | ||
const updatedQuery = query.replace('/client_interface/json/', '/client_interface/jsonp/'); | ||
const response = await fetchJsonp(updatedQuery, { | ||
jsonpCallback: 'callback', | ||
timeout: 10000 // 10 seconds timeout | ||
}); | ||
const data = await response.json(); | ||
if (!Array.isArray(data) || data.length === 0) { | ||
throw new Error('No data found'); | ||
} | ||
return data; | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
throw new Error(error.message || 'Error loading data'); | ||
} else { | ||
throw new Error('Error loading data'); | ||
} | ||
} | ||
} | ||
|
||
export function exportCSV(data: any[]): string { | ||
if (!Array.isArray(data) || data.length === 0) { | ||
throw new Error('No data found'); | ||
} | ||
const header = Object.keys(data[0]).join(','); | ||
const csvRows = data.map((row) => { | ||
const values = Object.values(row).map((value) => { | ||
if (typeof value === 'string') { | ||
let stringValue = value.replace(/"/g, '""'); // Escape double quotes | ||
if (stringValue.includes(',') || stringValue.includes('\n')) { | ||
stringValue = `"${value}"`; // Quote fields containing commas or newlines | ||
} | ||
return stringValue; | ||
} | ||
return String(value); | ||
}); | ||
return values.join(','); | ||
}); | ||
csvRows.unshift(header); // Add header row at the beginning | ||
|
||
const csvString = csvRows.join('\n'); | ||
const blob = new Blob([csvString], { type: 'text/csv' }); | ||
return URL.createObjectURL(blob); | ||
} | ||
|
||
export function exportKML(data: any[]): string { | ||
let kmlContent = `<?xml version="1.0" encoding="UTF-8"?> | ||
<kml xmlns="http://www.opengis.net/kml/2.2"> | ||
<Document>`; | ||
|
||
// Add place marks | ||
data.forEach((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 = prepareSimpleLine(meeting); | ||
const address = prepareSimpleLine(meeting, false); | ||
|
||
kmlContent += ` | ||
<Placemark> | ||
<name>${name}</name> | ||
${address ? `<address>${address}</address>` : ''} | ||
${description ? `<description>${description}</description>` : ''} | ||
<Point> | ||
<coordinates>${lng},${lat}</coordinates> | ||
</Point> | ||
</Placemark> | ||
`; | ||
}); | ||
|
||
kmlContent += ` | ||
</Document> | ||
</kml>`; | ||
|
||
const blob = new Blob([kmlContent], { type: 'application/vnd.google-earth.kml+xml' }); | ||
return URL.createObjectURL(blob); | ||
} | ||
|
||
function prepareSimpleLine(meeting: { [x: string]: any }, withDate = true): string { | ||
const getLocationInfo = () => { | ||
const locationInfo: any[] = []; | ||
const addInfo = (property: string) => { | ||
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 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.getTime())) { | ||
dateString += `, ${time.toLocaleTimeString('en-US', { | ||
hour: 'numeric', | ||
minute: 'numeric', | ||
hour12: true | ||
})}`; | ||
} | ||
|
||
return dateString; | ||
} | ||
|
||
return ''; | ||
}; | ||
|
||
const locationInfo = getLocationInfo(); | ||
const dateString = getDateString(); | ||
|
||
if (withDate && dateString && locationInfo) { | ||
return `${dateString}, ${locationInfo}`; | ||
} else if (dateString) { | ||
return dateString; | ||
} else { | ||
return locationInfo; | ||
} | ||
} |
Oops, something went wrong.