<!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 the Cities</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 class="container vh-100"> <h1>Name All the Cities</h1> <div class="mb-3"> <label for="state-select" class="form-label">State:</label> <div class="input-group"> <select id="state-select" class="form-select" disabled> <option select>Choose a stateā¦</option> </select> <button class="btn btn-primary" type="button" id="go-button">Play</button> </div> </div> <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> <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> </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"> Created by <a href="https://chandlerswift.com">Chandler Swift</a> using 2020 US Census data | <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>