This commit is contained in:
2026-03-25 16:10:17 -04:00
parent f3667266a9
commit 942da76704
933 changed files with 149047 additions and 2 deletions
@@ -0,0 +1,130 @@
/* Full disclosure, I am really bad at styling. Just not the creative type... */
/* Body and other basic elements styling */
body {
background-color: gray;
font-family: 'Courier New', Courier, monospace;
padding: 0;
margin: 0;
}
h1 {
text-align: center;
}
/* Nav styling */
nav ul {
list-style: none;
display: flex;
padding: 0;
margin: 0;
justify-content: center;
gap: 15px;
}
nav a {
text-decoration: none;
}
/* Inputs styling */
fieldset input {
max-width: 120px;
padding: 3px 5px;
margin-right: 5px;
}
/* Button styling */
button {
background-color: lime;
font-weight: bold;
padding: 5px 10px;
cursor: pointer;
border: 1px solid black;
transition: background-color 0.2;
}
button:hover {
background-color: #a6ff00;
}
#resetFilterBtn {
background-color: red;
}
#resetFilterBtn:hover {
background-color: #ff5555;
}
#downloadBtn{
display: block;
margin: 10px auto;
}
/* Fieldset legend and table captions */
legend, caption {
font-weight: bold;
}
/* Table Styling */
table {
border: 2px solid black;
background-color: white;
margin: 0px auto;
border-collapse: collapse;
width: 90%;
max-width: 1200px;
}
table td, table th {
border: 2px solid black;
padding: 6px 10px;
text-align: center;
}
table th {
background-color: #f0f0f0;
cursor: pointer;
}
table tr:nth-child(even) { /* Make every even row a different colour */
background-color: #f9f9f9;
}
table tr:hover {
background-color: #e0e0e0;
}
/* Map Styling */
#map {
border: 2px solid black;
height: 700px;
width: 90%;
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%;
}
}
@@ -0,0 +1,70 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Nasa Meteorite Explorer</title>
<link rel="stylesheet" href="css/style.css">
</head>
<body>
<header>
<h1>Nasa Meteorite Explorer</h1>
<nav>
<ul>
<li><a href="#map">Map</a></li>
<li><a href="#meteorTableBody">Table</a></li>
</ul>
</nav>
<hr>
</header>
<!-- Filter by year stuff -->
<fieldset>
<legend>Sort By Year</legend>
<label for="minYearInput">Min Year</label>
<input type="number" placeholder="MinYear" name="minYearInput" id="minYearInput">
<label for="maxYearInput">Max Year</label>
<input type="number" placeholder="MaxYear" name="maxYearInput" id="maxYearInput">
<button type="submit" value="Filter By Year" name="yearFilterBtn" onclick="filterByYear()">Filter By Year</button>
</fieldset>
<!-- Filter by name stuff -->
<fieldset>
<legend>Sort By Name</legend>
<label for="nameInput">Name</label>
<input type="text" placeholder="Name" name="nameInput" id="nameInput">
<button type="submit" value="Filter By Name" name="nameFilterBtn" onclick="filterByName()">Filter By Name</button>
<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 -->
<div id="map"></div>
<hr>
<!-- Table with clickable headers for sorting -->
<table id="meteorTable">
<caption>Meteorite Data Table</caption>
<thead>
<tr>
<th onclick="sortTable(0)">ID <span class="arrow"></span></th>
<th onclick="sortTable(1)">Name <span class="arrow"></span></th>
<th onclick="sortTable(2)">Year <span class="arrow"></span></th>
<th onclick="sortTable(3)">Recclass <span class="arrow"></span></th>
<th onclick="sortTable(4)">Mass <span class="arrow"></span></th>
</tr>
</thead>
<tbody id="meteorTableBody"></tbody>
</table><br>
<script src="js/main.js" onload="fetchJson()"></script>
<script src="js/map.js"></script>
<script src="js/dataHandler.js"></script>
<script src="https://maps.googleapis.com/maps/api/js?key=AIzaSyC1Mo5opxS1m0c16McSaTfzqnFAgbEuU2k&callback=loadMap" async defer></script>
</body>
</html>
@@ -0,0 +1,134 @@
let currentSortColumn = -1;
let ascending = true;
let filteredData = [];
// 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);
}
@@ -0,0 +1,25 @@
// Fetch the JSON data and put it into the table body
function fetchJson() {
fetch('./js/Meteorite_Landings.json')
.then((response) => response.json())
.then(data => {
console.log(data);
const tableBody = document.getElementById("meteorTableBody");
data.splice(0,35).forEach(meteor => { // Just get 500 values for now
const row = document.createElement("tr");
const id = document.createElement("td");
id.textContent = meteor.id ?? "-";
const name = document.createElement("td");
name.textContent = meteor.name ?? "-";
const year = document.createElement("td");
year.textContent = meteor.year ?? "-";
const recclass = document.createElement("td");
recclass.textContent = meteor.recclass ?? "-";
const mass = document.createElement("td");
mass.textContent = meteor["mass (g)"] + "g" ?? "-";
row.append(id, name, year, recclass, mass);
tableBody.appendChild(row);
})
})
.catch(error => console.error(error));
}
@@ -0,0 +1,64 @@
let map;
let meteorData = [];
let markers = [];
// Load the map from the Google Maps JS API
function loadMap() {
map = new google.maps.Map(document.getElementById("map"), {
center: {lat: 0, lng: 0}, // Center of the globe
zoom: 2
});
fetch("./js/Meteorite_Landings.json")
.then((response) => response.json())
.then(data => {
meteorData = data.splice(0,35);
filteredData = [...meteorData];
drawMarkers(filteredData);
})
.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 = [];
}