Add basic mapping
This commit is contained in:
parent
c16925231e
commit
bf11f90a34
298
index.html
298
index.html
|
@ -7,7 +7,7 @@
|
|||
<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 class="container">
|
||||
<div class="container vh-100">
|
||||
<h1>Name All the Cities</h1>
|
||||
<div class="mb-3">
|
||||
<label for="state-select" class="form-label">State:</label>
|
||||
|
@ -18,129 +18,50 @@
|
|||
<button class="btn btn-primary" type="button" id="go-button">Play</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="module">
|
||||
(async function init(){
|
||||
const response = await fetch("data/states.json");
|
||||
const states = await response.json();
|
||||
|
||||
const ss = document.getElementById('state-select');
|
||||
ss.disabled = false;
|
||||
for (let state of states) {
|
||||
ss.options[ss.options.length] = new Option(state);
|
||||
}
|
||||
|
||||
document.getElementById('go-button').addEventListener('click', async (e) => {
|
||||
e.target.disabled = true;
|
||||
ss.disabled = true;
|
||||
await selectState(ss.value);
|
||||
document.getElementById('game').style.display = 'block';
|
||||
});
|
||||
|
||||
// Check if there's a parameter in the URL already
|
||||
const state = (new URLSearchParams(window.location.search)).get('state');
|
||||
if (state) {
|
||||
ss.value = state;
|
||||
document.getElementById('go-button').click();
|
||||
}
|
||||
})();
|
||||
|
||||
import { createApp } from 'https://unpkg.com/petite-vue?module'
|
||||
|
||||
// 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, '');
|
||||
|
||||
async function selectState(stateName) {
|
||||
const response = await fetch(`data/${stateName}.json`);
|
||||
const cities = await response.json();
|
||||
console.log(cities)
|
||||
|
||||
window.app = createApp({
|
||||
state: stateName,
|
||||
cities: cities,
|
||||
simplified_cities: cities.map(city => simplify(city.name)),
|
||||
city_guess: "",
|
||||
message: "",
|
||||
achievements: {
|
||||
"Top Five": cities => cities.slice(0, 5),
|
||||
"Top Ten": cities => cities.slice(0, 10),
|
||||
"Top Twenty": cities => cities.slice(0, 20),
|
||||
"Top Fifty": cities => cities.slice(0, 50),
|
||||
"Top Hundred": cities => cities.slice(0, 100),
|
||||
"Every Single One": cities => cities,
|
||||
},
|
||||
guess() {
|
||||
const rank = this.simplified_cities.indexOf(simplify(this.city_guess))
|
||||
if (rank >= 0) {
|
||||
const city = this.cities[rank];
|
||||
|
||||
if (!city.guessed) {
|
||||
city.guessed = true;
|
||||
this.message = `${city.name} (population ${city.pop}) is the ${ordinal_suffix_of(rank + 1)} most populated city in ${this.state}.`;
|
||||
this.city_guess = "";
|
||||
} else {
|
||||
this.message = `Already guessed ${city.name} (population ${city.pop}, rank ${rank}).`;
|
||||
this.city_guess = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
}).mount()
|
||||
}
|
||||
</script>
|
||||
<div id="game" v-scope style="display: none;">
|
||||
<div class="mb-3">
|
||||
<label for="city-guess" class="form-label">City:</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="city-guess" v-model="city_guess" @keyup.enter="guess">
|
||||
<button class="btn btn-primary" type="button" @click="guess">Guess</button>
|
||||
<div id="game" v-scope class="row h-100" style="visibility:hidden;">
|
||||
<div class="col-4">
|
||||
<div class="mb-3">
|
||||
<label for="city-guess" class="form-label">City:</label>
|
||||
<div class="input-group">
|
||||
<input class="form-control" id="city-guess" v-model="city_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>
|
||||
<div v-if="message != ''" class="alert alert-secondary" role="alert">
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
<h3>{{ state }} cities</h3>
|
||||
<div>
|
||||
<span
|
||||
v-for="(required_cities, name) in achievements"
|
||||
:class="{ 'badge': true, 'me-1': true, 'text-bg-secondary': !required_cities(cities).every(c => c.guessed), 'text-bg-warning': required_cities(cities).every(c => c.guessed) }">
|
||||
{{ name }}
|
||||
({{ required_cities(cities).filter(c => c.guessed).length }}/{{ required_cities(cities).length }})
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Name</th>
|
||||
<th>Population</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(city, rank) in cities">
|
||||
<tr v-show="city.guessed">
|
||||
<td>{{ rank + 1 }}</td>
|
||||
<td>{{ city.name }}</td>
|
||||
<td>{{ city.pop }}</td>
|
||||
<h3>{{ state }} cities</h3>
|
||||
<div>
|
||||
<span
|
||||
v-for="(required_cities, name) in achievements"
|
||||
:class="{ 'badge': true, 'me-1': true, 'text-bg-secondary': !required_cities(cities).every(c => c.guessed), 'text-bg-warning': required_cities(cities).every(c => c.guessed) }">
|
||||
{{ name }}
|
||||
({{ required_cities(cities).filter(c => c.guessed).length }}/{{ required_cities(cities).length }})
|
||||
</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Rank</th>
|
||||
<th>Name</th>
|
||||
<th>Population</th>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="(city, rank) in cities">
|
||||
<tr v-show="city.guessed">
|
||||
<td>{{ rank + 1 }}</td>
|
||||
<td>{{ city.name }}</td>
|
||||
<td>{{ city.pop }}</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="col-8 h-100">
|
||||
<canvas id="canvas" class="mx-3 my-auto" @vue:mounted="draw" width="800" height="600"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="footer mt-auto py-3 text-center text-muted">
|
||||
|
@ -148,5 +69,140 @@
|
|||
<a href="https://git.chandlerswift.com/chandlerswift/name-all-cities-by-population-quiz">Source</a>
|
||||
(<a href="https://git.chandlerswift.com/chandlerswift/name-all-cities-by-population-quiz/src/branch/main/LICENSE">GPL3</a>)
|
||||
</footer>
|
||||
|
||||
<script type="module">
|
||||
let state_shape_data;
|
||||
let state_shape_data_request;
|
||||
(async function init(){
|
||||
const response = await fetch("data/states.json");
|
||||
const states = await response.json();
|
||||
|
||||
const ss = document.getElementById('state-select');
|
||||
ss.disabled = false;
|
||||
for (let state of states) {
|
||||
ss.options[ss.options.length] = new Option(state);
|
||||
}
|
||||
|
||||
document.getElementById('go-button').addEventListener('click', async (e) => {
|
||||
e.target.disabled = true;
|
||||
ss.disabled = true;
|
||||
await selectState(ss.value);
|
||||
document.getElementById('game').style.visibility = 'visible';
|
||||
});
|
||||
|
||||
// Check if there's a parameter in the URL already
|
||||
const state = (new URLSearchParams(window.location.search)).get('state');
|
||||
if (state) {
|
||||
ss.value = state;
|
||||
document.getElementById('go-button').click();
|
||||
}
|
||||
|
||||
const shape_response = await fetch("data/states.geojson");
|
||||
state_shape_data_request = shape_response.json().then(data => state_shape_data = data);
|
||||
})();
|
||||
|
||||
import { createApp } from 'https://unpkg.com/petite-vue?module'
|
||||
|
||||
// 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, '');
|
||||
|
||||
async function selectState(stateName) {
|
||||
const response = await fetch(`data/${stateName}.json`);
|
||||
const cities = await response.json();
|
||||
await state_shape_data_request; // We can't draw until this is in
|
||||
const state_shape = state_shape_data.features.find(f => f.properties.NAME.toLowerCase() == stateName.toLowerCase());
|
||||
const state_bounds = find_bounds(state_shape.geometry.coordinates[0]);
|
||||
if (!state_shape) {
|
||||
console.error("Unable to find state in shapes");
|
||||
}
|
||||
createApp({
|
||||
eventListenerAdded: false,
|
||||
state: stateName,
|
||||
cities: cities,
|
||||
simplified_cities: cities.map(city => simplify(city.name)),
|
||||
city_guess: "",
|
||||
message: "",
|
||||
achievements: {
|
||||
"Top Five": cities => cities.slice(0, 5),
|
||||
"Top Ten": cities => cities.slice(0, 10),
|
||||
"Top Twenty": cities => cities.slice(0, 20),
|
||||
"Top Fifty": cities => cities.slice(0, 50),
|
||||
"Top Hundred": cities => cities.slice(0, 100),
|
||||
"Every Single One": cities => cities,
|
||||
"All above 100k": cities => cities.filter(city => city.pop >= 100000),
|
||||
"All above 50k": cities => cities.filter(city => city.pop >= 50000),
|
||||
"All above 25k": cities => cities.filter(city => city.pop >= 25000),
|
||||
},
|
||||
guess() {
|
||||
const rank = this.simplified_cities.indexOf(simplify(this.city_guess))
|
||||
if (rank >= 0) {
|
||||
const city = this.cities[rank];
|
||||
|
||||
if (!city.guessed) {
|
||||
city.guessed = true;
|
||||
this.message = `${city.name} (population ${city.pop}) is the ${ordinal_suffix_of(rank + 1)} most populated city in ${this.state}.`;
|
||||
this.city_guess = "";
|
||||
this.draw();
|
||||
} else {
|
||||
this.message = `Already guessed ${city.name} (population ${city.pop}, rank ${rank}).`;
|
||||
this.city_guess = "";
|
||||
}
|
||||
}
|
||||
},
|
||||
draw() {
|
||||
const canvas = document.getElementById("canvas");
|
||||
const ctx = canvas.getContext("2d");
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
function transform(pt) {
|
||||
// TODO: this just works for MN, and not very well at that
|
||||
return [(pt[0]+98)*80, 600-(pt[1]-42)*80];
|
||||
}
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(...transform(state_shape.geometry.coordinates[0][0]))
|
||||
console.log(transform(state_shape.geometry.coordinates[0][0]))
|
||||
for (let coord of state_shape.geometry.coordinates[0].slice(1)) {
|
||||
ctx.lineTo(...transform(coord));
|
||||
}
|
||||
ctx.stroke();
|
||||
for (let city of cities) {
|
||||
if (city.guessed) {
|
||||
ctx.beginPath();
|
||||
const c = transform([city.location[1], city.location[0]])
|
||||
console.log(city.location, c)
|
||||
ctx.arc(c[0], c[1], 2, 0, 2*Math.PI, true);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
},
|
||||
}).mount();
|
||||
}
|
||||
|
||||
// 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>
|
||||
|
|
Loading…
Reference in a new issue