189 lines
8.4 KiB
HTML
189 lines
8.4 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" class="h-100">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Name All MN's Lakes</title>
|
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-QWTKZyjpPEjISv5WaRU9OFeRpok6YctnYmDr5pNlyT2bRjXh0JMhjY6hW+ALEwIH" crossorigin="anonymous">
|
|
</head>
|
|
<body class="h-100 d-flex flex-column">
|
|
<div v-scope @vue:mounted="mounted" class="container vh-100">
|
|
<h1>Name All MN's Lakes</h1>
|
|
<div class="row">
|
|
<div class="col-4">
|
|
<div class="mb-3">
|
|
<label for="lake-guess" class="form-label">Lake:</label>
|
|
<div class="input-group">
|
|
<input class="form-control" id="lake-guess" v-model="lake_guess" @keyup.enter="guess">
|
|
<button class="btn btn-primary" type="button" @click="guess">Guess</button>
|
|
</div>
|
|
</div>
|
|
<div v-if="message != ''" class="alert alert-secondary" role="alert">
|
|
{{ message }}
|
|
</div>
|
|
|
|
<div>
|
|
<span
|
|
v-for="(required_lakes, name) in achievements"
|
|
:class="{ 'badge': true, 'me-1': true, 'text-bg-secondary': !required_lakes(lakes).every(c => c.guessed), 'text-bg-warning': required_lakes(lakes).every(c => c.guessed) }">
|
|
{{ name }}
|
|
({{ required_lakes(lakes).filter(c => c.guessed).length }}/{{ required_lakes(lakes).length }})
|
|
</span>
|
|
</div>
|
|
<table class="table">
|
|
<thead>
|
|
<tr>
|
|
<th>Rank</th>
|
|
<th>Name</th>
|
|
<th>Count</th>
|
|
<th>Total Area</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="(lake, rank) in lakes">
|
|
<tr v-show="lake.guessed">
|
|
<td>{{ rank + 1 }}</td>
|
|
<td>{{ lake.name }}</td>
|
|
<td>{{ lake.centers.length }}</td>
|
|
<td>{{ lake.area.toFixed(3) }}</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
<div class="col-8">
|
|
<canvas id="canvas" class="mx-3 my-auto" width="800" height="600"></canvas>
|
|
<div>
|
|
<input type="checkbox" id="show-unguessed-checkbox" v-model="show_unguessed_lakes" @change="draw">
|
|
<label for="show-unguessed-checkbox">Show unguessed lakes</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<footer class="footer mt-auto py-3 text-center text-muted">
|
|
Created by <a href="https://chandlerswift.com">Chandler Swift</a> using data from MN DNR |
|
|
<a href="https://git.chandlerswift.com/chandlerswift/name-all-lakes-by-population-quiz">Source</a>
|
|
(<a href="https://git.chandlerswift.com/chandlerswift/name-all-lakes-by-population-quiz/src/branch/main/LICENSE">GPL3</a>)
|
|
</footer>
|
|
|
|
<script type="module">
|
|
import { createApp } from 'https://unpkg.com/petite-vue?module';
|
|
|
|
const response = await fetch(`lakes.json`);
|
|
const lakes = await response.json();
|
|
createApp({
|
|
lakes: lakes,
|
|
bounds: null,
|
|
lake_guess: "",
|
|
message: "",
|
|
show_unguessed_lakes: true,
|
|
achievements: {
|
|
"Top Five": lakes => lakes.slice(0, 5),
|
|
"Top Ten": lakes => lakes.slice(0, 10),
|
|
"Top Twenty": lakes => lakes.slice(0, 20),
|
|
"Top Fifty": lakes => lakes.slice(0, 50),
|
|
"Top Hundred": lakes => lakes.slice(0, 100),
|
|
"Every Single One": lakes => lakes,
|
|
"All above 1M acres": lakes => lakes.filter(lake => lake.area >= 1000000),
|
|
"All above 100k acres": lakes => lakes.filter(lake => lake.area >= 100000),
|
|
"All above 50k acres": lakes => lakes.filter(lake => lake.area >= 50000),
|
|
"All above 25k acres": lakes => lakes.filter(lake => lake.area >= 25000),
|
|
"With >100 lakes": lakes => lakes.filter(lake => lake.centers.length >= 100),
|
|
"With >50 lakes": lakes => lakes.filter(lake => lake.centers.length >= 50),
|
|
"With >25 lakes": lakes => lakes.filter(lake => lake.centers.length >= 25),
|
|
},
|
|
async mounted() {
|
|
let all_centers = [];
|
|
for (let lake of lakes) {
|
|
all_centers.push(...lake.centers);
|
|
}
|
|
|
|
this.bounds = find_bounds(all_centers);
|
|
console.log(this.lakes[0].name);
|
|
this.simplified_lakes = this.lakes.map(lake => simplify(lake.name));
|
|
this.draw();
|
|
},
|
|
guess() {
|
|
const rank = this.simplified_lakes.indexOf(simplify(this.lake_guess))
|
|
if (rank >= 0) {
|
|
const lake = this.lakes[rank];
|
|
|
|
if (!lake.guessed) {
|
|
lake.guessed = true;
|
|
this.message = `${lake.name} (total area of ${lake.centers.length} lakes ${lake.area.toFixed(3)} acres) is the ${ordinal_suffix_of(rank + 1)} largest area set of lakes in MN.`;
|
|
this.lake_guess = "";
|
|
this.draw();
|
|
} else {
|
|
this.message = `Already guessed ${lake.name} (${lake.area.toFixed(3)} acres on ${lake.centers.length} lakes, rank ${rank + 1}).`;
|
|
this.lake_guess = "";
|
|
}
|
|
}
|
|
},
|
|
draw() {
|
|
const canvas = document.getElementById("canvas");
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
|
|
|
const [minx, maxx, miny, maxy] = this.bounds;
|
|
const height_scale = (canvas.height - 8) / (maxy - miny);
|
|
const width_scale = (canvas.width - 8) / (maxx - minx);
|
|
const scale = Math.min(height_scale, width_scale);
|
|
|
|
const y_offset = miny - (canvas.height / scale - (maxy - miny)) / 2;
|
|
const x_offset = minx - (canvas.width / scale - (maxx - minx)) / 2;
|
|
|
|
function transform(pt) {
|
|
return [scale * (pt[0] - x_offset), canvas.height - (scale * (pt[1] - y_offset))];
|
|
}
|
|
|
|
for (let lake of this.lakes) {
|
|
for (let center of lake.centers) {
|
|
if (lake.guessed) {
|
|
ctx.beginPath();
|
|
const c = transform(center)
|
|
ctx.fillStyle = "black";
|
|
ctx.arc(c[0], c[1], 2, 0, 2*Math.PI, true);
|
|
ctx.fill();
|
|
} else if (this.show_unguessed_lakes) {
|
|
ctx.beginPath();
|
|
const c = transform(center)
|
|
ctx.fillStyle = "lightgray";
|
|
ctx.arc(c[0], c[1], 2, 0, 2*Math.PI, true);
|
|
ctx.fill();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}).mount();
|
|
|
|
// https://stackoverflow.com/a/13627586
|
|
function ordinal_suffix_of(i) {
|
|
let j = i % 10,
|
|
k = i % 100;
|
|
if (j === 1 && k !== 11) {
|
|
return i + "st";
|
|
}
|
|
if (j === 2 && k !== 12) {
|
|
return i + "nd";
|
|
}
|
|
if (j === 3 && k !== 13) {
|
|
return i + "rd";
|
|
}
|
|
return i + "th";
|
|
}
|
|
|
|
const simplify = s => s.toLowerCase().replace(/[^a-z]/g, '');
|
|
|
|
// returns minx, maxx, miny, maxy
|
|
function find_bounds(coords) {
|
|
return [
|
|
Math.min(...coords.map(c => c[0])),
|
|
Math.max(...coords.map(c => c[0])),
|
|
Math.min(...coords.map(c => c[1])),
|
|
Math.max(...coords.map(c => c[1]))
|
|
];
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|