Merge branch 'main' of https://git.levimclean.me/Levi/IWD2-02
This commit is contained in:
@@ -82,6 +82,7 @@ table td, table th {
|
|||||||
|
|
||||||
table th {
|
table th {
|
||||||
background-color: #f0f0f0;
|
background-color: #f0f0f0;
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
table tr:nth-child(even) { /* Make every even row a different colour */
|
table tr:nth-child(even) { /* Make every even row a different colour */
|
||||||
@@ -99,3 +100,31 @@ table tr:hover {
|
|||||||
width: 90%;
|
width: 90%;
|
||||||
margin: 20px auto;
|
margin: 20px auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Arrow Styling */
|
||||||
|
.arrow {
|
||||||
|
margin-left: 4px;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Small mobile responsiveness changes */
|
||||||
|
@media (max-width: 625px) {
|
||||||
|
fieldset {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Make inputs full width on mobile */
|
||||||
|
fieldset input {
|
||||||
|
max-width: 100%;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons stack nicely on mobile */
|
||||||
|
fieldset button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,46 +23,47 @@
|
|||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Sort By Year</legend>
|
<legend>Sort By Year</legend>
|
||||||
<label for="minYearInput">Min Year</label>
|
<label for="minYearInput">Min Year</label>
|
||||||
<input type="number" placeholder="MinYear" name="minYearInput">
|
<input type="number" placeholder="MinYear" name="minYearInput" id="minYearInput">
|
||||||
|
|
||||||
<label for="maxYearInput">Max Year</label>
|
<label for="maxYearInput">Max Year</label>
|
||||||
<input type="number" placeholder="MaxYear" name="maxYearInput">
|
<input type="number" placeholder="MaxYear" name="maxYearInput" id="maxYearInput">
|
||||||
|
|
||||||
|
<button type="submit" value="Filter By Year" name="yearFilterBtn" onclick="filterByYear()">Filter By Year</button>
|
||||||
|
|
||||||
<button type="submit" value="Filter By Year" name="yearFilterBtn">Filter By Year</button>
|
|
||||||
<button type="reset" value="Reset Filter" name="resetFilterBtn" id="resetFilterBtn">Reset Filter</button>
|
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
<!-- Filter by name stuff -->
|
<!-- Filter by name stuff -->
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Sort By Name</legend>
|
<legend>Sort By Name</legend>
|
||||||
<label for="nameInput">Name</label>
|
<label for="nameInput">Name</label>
|
||||||
<input type="text" placeholder="Name" name="nameInput">
|
<input type="text" placeholder="Name" name="nameInput" id="nameInput">
|
||||||
|
|
||||||
<button type="submit" value="Filter By Name" name="nameFilterBtn">Filter By Name</button>
|
<button type="submit" value="Filter By Name" name="nameFilterBtn" onclick="filterByName()">Filter By Name</button>
|
||||||
</fieldset><hr>
|
<button type="reset" value="Reset Filter" name="resetFilterBtn" id="resetFilterBtn" onclick="resetFilters()">Reset Filters</button>
|
||||||
|
</fieldset>
|
||||||
|
<button type="button" name="downloadBtn" id="downloadBtn" onclick="downloadData()">Download Filtered Data</button>
|
||||||
|
<hr>
|
||||||
|
|
||||||
<!-- Where Google Map will go -->
|
<!-- Where Google Map will go -->
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<!-- Table with clickable headers for sorting -->
|
<!-- Table with clickable headers for sorting -->
|
||||||
<table>
|
<table id="meteorTable">
|
||||||
<caption>Meteorite Data Table</caption>
|
<caption>Meteorite Data Table</caption>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>ID</th>
|
<th onclick="sortTable(0)">ID <span class="arrow"></span></th>
|
||||||
<th>Name</th>
|
<th onclick="sortTable(1)">Name <span class="arrow"></span></th>
|
||||||
<th>Year</th>
|
<th onclick="sortTable(2)">Year <span class="arrow"></span></th>
|
||||||
<th>Recclass</th>
|
<th onclick="sortTable(3)">Recclass <span class="arrow"></span></th>
|
||||||
<th>Mass</th>
|
<th onclick="sortTable(4)">Mass <span class="arrow"></span></th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="meteorTableBody"></tbody>
|
<tbody id="meteorTableBody"></tbody>
|
||||||
</table><br>
|
</table><br>
|
||||||
|
|
||||||
<button type="button" name="downloadBtn" id="downloadBtn">Download Filtered Data</button>
|
<script src="js/main.js" onload="fetchJson()"></script>
|
||||||
|
|
||||||
<script src="js/main.js"></script>
|
|
||||||
<script src="js/map.js"></script>
|
<script src="js/map.js"></script>
|
||||||
<script src="js/dataHandler.js"></script>
|
<script src="js/dataHandler.js"></script>
|
||||||
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC1Mo5opxS1m0c16McSaTfzqnFAgbEuU2k&callback=loadMap" async defer></script>
|
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC1Mo5opxS1m0c16McSaTfzqnFAgbEuU2k&callback=loadMap" async defer></script>
|
||||||
|
|||||||
@@ -1,4 +1,134 @@
|
|||||||
// TODO:
|
let currentSortColumn = -1;
|
||||||
// Implement Search by year and name
|
let ascending = true;
|
||||||
// Reset filters button
|
let filteredData = [];
|
||||||
// Clickable table headers for sortin (reuse code?)
|
|
||||||
|
// Clickable table headers for sorting
|
||||||
|
function sortTable(col) {
|
||||||
|
const table = document.getElementById("meteorTable");
|
||||||
|
const tbody = table.tBodies[0];
|
||||||
|
const rows = Array.from(tbody.rows);
|
||||||
|
|
||||||
|
// If the column that was clicked is the same as the previous one clicked, reverse sort direction
|
||||||
|
if (col === currentSortColumn) {
|
||||||
|
ascending = !ascending;
|
||||||
|
} else {
|
||||||
|
ascending = true;
|
||||||
|
currentSortColumn = col;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update arrows for column clicked on
|
||||||
|
updateArrows(col);
|
||||||
|
|
||||||
|
// Actual sorting of the table rows
|
||||||
|
rows.sort((rowA, rowB) => {
|
||||||
|
let a = rowA.cells[col].textContent.trim();
|
||||||
|
let b = rowB.cells[col].textContent.trim();
|
||||||
|
|
||||||
|
let numA = Number(a);
|
||||||
|
let numB = Number(b);
|
||||||
|
|
||||||
|
if (!isNaN(numA) && !isNaN(numB)) {
|
||||||
|
return ascending ? numA - numB : numB - numA;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (a < b) return ascending ? -1 : 1;
|
||||||
|
if (a > b) return ascending ? 1 : -1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Append sorted rows to table body
|
||||||
|
for (let row of rows) {
|
||||||
|
tbody.appendChild(row);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to swap arrow symbols on click
|
||||||
|
function updateArrows(col) {
|
||||||
|
const arrows = document.querySelectorAll("th .arrow");
|
||||||
|
|
||||||
|
arrows.forEach(arrow => arrow.textContent = "");
|
||||||
|
|
||||||
|
arrows[col].textContent = ascending ? "▲" : "▼";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to sort data by Name
|
||||||
|
function filterByName() {
|
||||||
|
// Get the user input in all lowercase
|
||||||
|
const input = document.getElementById("nameInput").value.trim().toLowerCase();
|
||||||
|
|
||||||
|
// Use the spread operator to turn the meteorData into an array, then filter by input
|
||||||
|
if (input === "") {
|
||||||
|
filteredData = [...meteorData];
|
||||||
|
} else {
|
||||||
|
filteredData = meteorData.filter(meteor =>
|
||||||
|
meteor.name && meteor.name.toLowerCase().includes(input)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Redraw markers with only the filtered data
|
||||||
|
drawMarkers(filteredData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to sort by year
|
||||||
|
function filterByYear() {
|
||||||
|
// Get min and max year inputs
|
||||||
|
const minYear = Number(document.getElementById("minYearInput").value);
|
||||||
|
const maxYear = Number(document.getElementById("maxYearInput").value);
|
||||||
|
|
||||||
|
// Once again, filter the meteorData, this time within a range of minYear-maxYear if present
|
||||||
|
filteredData = meteorData.filter(meteor => {
|
||||||
|
const year = parseInt(meteor.year);
|
||||||
|
if (isNaN(year)) return false;
|
||||||
|
|
||||||
|
if (!isNaN(minYear) && year < minYear) return false;
|
||||||
|
if (!isNaN(maxYear) && year > maxYear) return true;
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
// And redraw filtered data by year
|
||||||
|
drawMarkers(filteredData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to reset filters
|
||||||
|
function resetFilters() {
|
||||||
|
filteredData = [...meteorData];
|
||||||
|
drawMarkers(filteredData); // Just reload the map without any sorting
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to download filtered data
|
||||||
|
function downloadData() {
|
||||||
|
// Do nothing if data hasn't been modified
|
||||||
|
if (!filteredData || filteredData.length === 0) {
|
||||||
|
alert("No data to download.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a string of JSON from the filtered data
|
||||||
|
const jsonString = JSON.stringify(filteredData, null, 2);
|
||||||
|
|
||||||
|
// Create a binary blob from the JSON
|
||||||
|
const blob = new Blob([jsonString], {type: "application/json" });
|
||||||
|
|
||||||
|
// Create a URL from the blob object
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
|
||||||
|
// Bit hacky, but I create an <a> element for the download link
|
||||||
|
const a = document.createElement("a");
|
||||||
|
|
||||||
|
// Set the Href of <a> to the URL of the blob
|
||||||
|
a.href = url;
|
||||||
|
|
||||||
|
// Set the downloaded file name for the JSON
|
||||||
|
a.download = "filtered_meteor_data.json";
|
||||||
|
|
||||||
|
// Append the <a> to the document
|
||||||
|
document.body.appendChild(a);
|
||||||
|
|
||||||
|
// Click the <a> element
|
||||||
|
a.click();
|
||||||
|
|
||||||
|
// Remove the <a> element and revoke the URL object
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
}
|
||||||
@@ -1,22 +1,25 @@
|
|||||||
fetch('./js/Meteorite_Landings.json')
|
// Fetch the JSON data and put it into the table body
|
||||||
.then((response) => response.json())
|
function fetchJson() {
|
||||||
.then(data => {
|
fetch('./js/Meteorite_Landings.json')
|
||||||
console.log(data);
|
.then((response) => response.json())
|
||||||
const tableBody = document.getElementById("meteorTableBody");
|
.then(data => {
|
||||||
data.splice(0,500).forEach(meteor => { // Just get 500 values for now
|
console.log(data);
|
||||||
const row = document.createElement("tr");
|
const tableBody = document.getElementById("meteorTableBody");
|
||||||
const id = document.createElement("td");
|
data.splice(0,500).forEach(meteor => { // Just get 500 values for now
|
||||||
id.textContent = meteor.id ?? "-";
|
const row = document.createElement("tr");
|
||||||
const name = document.createElement("td");
|
const id = document.createElement("td");
|
||||||
name.textContent = meteor.name ?? "-";
|
id.textContent = meteor.id ?? "-";
|
||||||
const year = document.createElement("td");
|
const name = document.createElement("td");
|
||||||
year.textContent = meteor.year ?? "-";
|
name.textContent = meteor.name ?? "-";
|
||||||
const recclass = document.createElement("td");
|
const year = document.createElement("td");
|
||||||
recclass.textContent = meteor.recclass ?? "-";
|
year.textContent = meteor.year ?? "-";
|
||||||
const mass = document.createElement("td");
|
const recclass = document.createElement("td");
|
||||||
mass.textContent = meteor["mass (g)"] + "g" ?? "-";
|
recclass.textContent = meteor.recclass ?? "-";
|
||||||
row.append(id, name, year, recclass, mass);
|
const mass = document.createElement("td");
|
||||||
tableBody.appendChild(row);
|
mass.textContent = meteor["mass (g)"] + "g" ?? "-";
|
||||||
|
row.append(id, name, year, recclass, mass);
|
||||||
|
tableBody.appendChild(row);
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
.catch(error => console.error(error));
|
||||||
.catch(error => console.error(error));
|
}
|
||||||
@@ -1,39 +1,64 @@
|
|||||||
|
let map;
|
||||||
|
let meteorData = [];
|
||||||
|
let markers = [];
|
||||||
|
|
||||||
// Load the map from the Google Maps JS API
|
// Load the map from the Google Maps JS API
|
||||||
function loadMap() {
|
function loadMap() {
|
||||||
const map = new google.maps.Map(document.getElementById("map"), {
|
map = new google.maps.Map(document.getElementById("map"), {
|
||||||
center: {lat: 0, lng: 0}, // Center of the globe
|
center: {lat: 0, lng: 0}, // Center of the globe
|
||||||
zoom: 2
|
zoom: 2
|
||||||
});
|
});
|
||||||
|
|
||||||
const infoWindow = new google.maps.InfoWindow(); // create InfoWindow object to use later
|
|
||||||
|
|
||||||
// Add custom markers from meteor data
|
|
||||||
fetch("./js/Meteorite_Landings.json")
|
fetch("./js/Meteorite_Landings.json")
|
||||||
.then((response) => response.json())
|
.then((response) => response.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
data.splice(0,500).forEach(location => {
|
meteorData = data.splice(0,500);
|
||||||
const marker = new google.maps.Marker({
|
filteredData = [...meteorData];
|
||||||
position: { lat: location.reclat, lng: location.reclong},
|
drawMarkers(filteredData);
|
||||||
map: map,
|
|
||||||
title: location.name
|
|
||||||
});
|
|
||||||
|
|
||||||
marker.addListener("click", () => { // Open and show the InfoWindow on click
|
|
||||||
const content = `
|
|
||||||
<div class="info-window">
|
|
||||||
<h3>${location.name}</h3>
|
|
||||||
<p><strong>Mass:</strong> ${location["mass (g)"]} g</p>
|
|
||||||
<p><strong>Year:</strong> ${location.year}</p>
|
|
||||||
<p><strong>Class:</strong> ${location.recclass}</p>
|
|
||||||
<p><strong>Fall Status:</strong> ${location.fall}</p>
|
|
||||||
<p><strong>Recorded Latitude:</strong> ${location.reclat}</p>
|
|
||||||
<p><strong>Recorded Longitude:</strong> ${location.reclong}</p>
|
|
||||||
</div>`; // By doing this I have more control over what I put in the InfoWindow and how to style it
|
|
||||||
infoWindow.setContent(content);
|
|
||||||
infoWindow.open(map, marker);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
.catch(error => console.error(error));
|
.catch(error => console.error(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
|
function drawMarkers(data) {
|
||||||
|
clearMarkers();
|
||||||
|
const infoWindow = new google.maps.InfoWindow(); // create InfoWindow object to use later
|
||||||
|
|
||||||
|
// Add custom markers from meteor data
|
||||||
|
data.forEach(location => {
|
||||||
|
const lat = Number(location.reclat);
|
||||||
|
const lng = Number(location.reclong);
|
||||||
|
|
||||||
|
// Ignore entries that are not numbers (will cause errors)
|
||||||
|
if (isNaN(lat) || isNaN(lng)) return;
|
||||||
|
|
||||||
|
// Create marker from reclat and reclong
|
||||||
|
const marker = new google.maps.Marker({
|
||||||
|
position: { lat: lat, lng: lng},
|
||||||
|
map: map,
|
||||||
|
title: location.name
|
||||||
|
});
|
||||||
|
|
||||||
|
marker.addListener("click", () => { // Open and show the InfoWindow on click
|
||||||
|
const content = `
|
||||||
|
<div class="info-window">
|
||||||
|
<h3>${location.name}</h3>
|
||||||
|
<p><strong>Mass:</strong> ${location["mass (g)"]} g</p>
|
||||||
|
<p><strong>Year:</strong> ${location.year}</p>
|
||||||
|
<p><strong>Class:</strong> ${location.recclass}</p>
|
||||||
|
<p><strong>Fall Status:</strong> ${location.fall}</p>
|
||||||
|
<p><strong>Recorded Latitude:</strong> ${lat}</p>
|
||||||
|
<p><strong>Recorded Longitude:</strong> ${lng}</p>
|
||||||
|
</div>`; // By doing this I have more control over what I put in the InfoWindow and how to style it
|
||||||
|
infoWindow.setContent(content);
|
||||||
|
infoWindow.open(map, marker);
|
||||||
|
});
|
||||||
|
|
||||||
|
markers.push(marker);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to clear markers for use in drawMarkers()
|
||||||
|
function clearMarkers() {
|
||||||
|
markers.forEach(marker => marker.setMap(null));
|
||||||
|
markers = [];
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user