├── LICENSE ├── README.md ├── data ├── Trade Gothic LT Bold.ttf ├── cities_nat_500k3s.geojson ├── cities_nat_500k3s.js ├── dot-logo.svg ├── favicon.svg ├── top-logo.svg ├── web-card.svg └── webcard.png ├── index.html └── main.js /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Jeff Allen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | ## city-guesser 3 | 4 | ![web card](data/webcard.png) 5 | 6 | City-Guesser is a map-based quiz game designed to test one's knowledge of urban form and transportation network structure of cities around the world. 7 | 8 | Play here: http://schoolofcities.github.io/city-guesser/index.html 9 | 10 | The game is simple. You are given an unlabeled map, and you have to guess which city it represents. If you guess correctly, you move onto the next round. Each round gets progressively more difficult as more obscure cities are added into the choice set. A level counter and game score is updated each time you guess correctly. If you guess wrong, you have to restart, but a high score is saved for future reference. 11 | 12 | The map was designed and built using Mapbox and QGIS, with data from OpenStreetMap and Natural Earth. 13 | 14 | -------------------------------------------------------------------------------- /data/Trade Gothic LT Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamaps/city-guesser/454e81d8c252cb3cfef81c0d9c08aff5917a0f63/data/Trade Gothic LT Bold.ttf -------------------------------------------------------------------------------- /data/dot-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 18 | 38 | 44 | 49 | 50 | 55 | 59 | 63 | 67 | 71 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 107 | 111 | 115 | 119 | 123 | 127 | 131 | 135 | 139 | 143 | 147 | 151 | 155 | 159 | 163 | 167 | 168 | 169 | 170 | -------------------------------------------------------------------------------- /data/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 37 | 39 | 43 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /data/top-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 16 | 18 | 37 | 43 | 48 | 49 | 54 | 58 | 62 | 66 | 70 | 74 | 78 | 82 | 86 | 90 | 94 | 98 | 102 | 106 | 110 | 114 | 118 | 122 | 126 | 130 | 134 | 138 | 142 | 146 | 150 | 154 | 158 | 162 | 166 | 170 | 174 | 178 | 182 | 186 | 190 | 194 | 198 | 202 | 206 | 210 | 214 | 218 | 222 | 223 | 224 | 225 | -------------------------------------------------------------------------------- /data/webcard.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamaps/city-guesser/454e81d8c252cb3cfef81c0d9c08aff5917a0f63/data/webcard.png -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | City-Guesser 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 156 | 157 | 158 | 159 | 160 | 161 |
162 | 163 |
164 | 165 |
166 | 167 |

City-Guesser

168 | School of Cities 169 |
170 |
171 |

 Level:

1

   Score:

0


172 |
173 | 174 |
175 |
176 |


177 |


178 |


179 |


180 |
181 | 182 |

Good Luck!

183 |
184 | 185 |
186 | 187 |
188 |

 Max Level Achieved:

0


189 |

 High Score:

0


190 | 191 |
192 |

Level 1-5:  Beginner


193 |

Level 6-10:  Occasional Traveller


194 |

Level 11-15:  Map Enthusiast


195 |

Level 16-20:  Globe-Trotter


196 |

Level 21-30:  Probably a Cartographer


197 |

Level 31 & Up:  PhD in Geography

198 |
199 | 200 |
201 | 202 |
203 |

204 | Source code for this map is on GitHub.
It was designed and built by Jeff Allen in 2017 (updated in 2022 at the School of Cities) using Mapbox and QGIS, with data from OpenStreetMap and Natural Earth. 205 |

206 |
207 | 208 |
209 | 210 |
211 | 212 | 213 | 214 | 215 | 216 | 217 | -------------------------------------------------------------------------------- /main.js: -------------------------------------------------------------------------------- 1 | // initial global vars 2 | 3 | var clevel = 1 4 | var remap = 0 5 | var score = 0 6 | var w = "" 7 | var choiceCities = [] 8 | mapboxgl.accessToken = 'meow'; 9 | 10 | mapboxgl.accessToken = 'pk.eyJ1Ijoic2Nob29sb2ZjaXRpZXMiLCJhIjoiY2w2bnFhaWJrMDNibjNqdGZibmhtNXpxbyJ9.ogVJPKMFm_JGVv8wNDsi9A'; 11 | 12 | 13 | // function for a random int within a range 14 | 15 | function getRandomInt(min, max) { 16 | return Math.floor(Math.random() * (max - min + 1)) + min; 17 | } 18 | 19 | 20 | // initialize the map 21 | 22 | var map = new mapboxgl.Map({ 23 | container: 'map', // container id 24 | style: 'mapbox://styles/schoolofcities/cl6nnmkks003q14pax679hpw4', // stylesheet location 25 | center: [0,0], // starting position [lng, lat] 26 | zoom: 12, // starting zoom 27 | maxZoom: 16, 28 | minZoom: 11.05, 29 | // pitchWithRotate: false, 30 | maxBounds: [[-0.42,-0.42],[0.42,0.42]] // Sets bounds as max 31 | }); 32 | 33 | nav = new mapboxgl.NavigationControl(); 34 | map.addControl(nav, 'top-left'); 35 | 36 | bar = new mapboxgl.ScaleControl({ 37 | maxWidth: 100, 38 | unit: 'metric' 39 | }); 40 | map.addControl(bar); 41 | 42 | 43 | 44 | // function for showing the map given a level and stage 45 | 46 | function showMap(level) { 47 | 48 | // generating an array of cities for this level, based on the level, stage criteria 49 | 50 | level_array = [] 51 | 52 | if (level < 11) { 53 | for (var c in cities["features"]) { 54 | if (cities["features"][c]["properties"]["WORLDCITY"] > 0) { 55 | level_array.push(cities["features"][c]) 56 | } 57 | } 58 | } else if (level >= 11 && level < 21) { 59 | for (var c in cities["features"]) { 60 | if (cities["features"][c]["properties"]["ADM0CAP"] > 0 || cities["features"][c]["properties"]["WORLDCITY"] > 0) { 61 | level_array.push(cities["features"][c]) 62 | } 63 | } 64 | } 65 | else if (level >= 21 && level < 31) { 66 | for (var c in cities["features"]) { 67 | if (cities["features"][c]["properties"]["SCALERANK"] <= 3) { 68 | level_array.push(cities["features"][c]) 69 | } 70 | } 71 | } else if (level >= 31 && level < 41) { 72 | for (var c in cities["features"]) { 73 | if (cities["features"][c]["properties"]["GN_POP"] > 999999 || cities["features"][c]["properties"]["ADM0CAP"] > 0) { 74 | level_array.push(cities["features"][c]) 75 | } 76 | } 77 | } else if (level >= 41 && level < 51) { 78 | for (var c in cities["features"]) { 79 | if (cities["features"][c]["properties"]["MEGACITY"] > 0) { 80 | level_array.push(cities["features"][c]) 81 | } 82 | } 83 | } else if (level >= 51 && level < 71) 84 | for (var c in cities["features"]) { 85 | if (cities["features"][c]["properties"]["SCALERANK"] < 5) { 86 | level_array.push(cities["features"][c]) 87 | } 88 | } 89 | else { 90 | for (var c in cities["features"]) { 91 | if (cities["features"][c]["properties"]["SCALERANK"] < 12) { 92 | level_array.push(cities["features"][c]) 93 | } 94 | } 95 | }; 96 | 97 | // remove cities that have already been selected 98 | 99 | level_array = level_array.filter( ( el ) => !choiceCities.includes( el ) ); 100 | 101 | 102 | // a random 4 cities from the array of cities 103 | 104 | rando_m0 = getRandomInt(0, level_array.length - 1) 105 | rando_m1 = getRandomInt(0, level_array.length - 1) 106 | while (rando_m1 === rando_m0) { 107 | rando_m1 = getRandomInt(0, level_array.length - 1) 108 | } 109 | rando_m2 = getRandomInt(0, level_array.length - 1) 110 | while (rando_m2 === rando_m0 || rando_m2 === rando_m1) { 111 | rando_m2 = getRandomInt(0, level_array.length - 1) 112 | } 113 | rando_m3 = getRandomInt(0, level_array.length - 1) 114 | while (rando_m3 === rando_m0 || rando_m3 === rando_m1 || rando_m3 === rando_m2) { 115 | rando_m3 = getRandomInt(0, level_array.length - 1) 116 | } 117 | 118 | 119 | 120 | // setting up an array of just these citeis 121 | 122 | var cities_select = [level_array[rando_m0],level_array[rando_m1],level_array[rando_m2],level_array[rando_m3]] 123 | 124 | // random city of these 4 125 | 126 | rando_i = getRandomInt(0, 3) 127 | 128 | p_score = score + 100 * (5 + 5 * (1 - cities_select[rando_i]["properties"]["WORLDCITY"]) + parseInt(cities_select[rando_i]["properties"]["SCALERANK"])) 129 | 130 | 131 | choiceCities.push(cities_select[rando_i]) 132 | 133 | 134 | // setting up the map to centre on a City 135 | 136 | var cx = cities_select[rando_i]["geometry"]["coordinates"][0] 137 | var cy = cities_select[rando_i]["geometry"]["coordinates"][1] 138 | 139 | w = cities_select[rando_i]["properties"]["NAME"] 140 | 141 | var bounds = [ 142 | [cx - 0.42, cy - 0.42], // Southwest 143 | [cx + 0.42, cy + 0.42] // Northeast 144 | ]; 145 | 146 | 147 | 148 | map.dragRotate._pitchWithRotate = false; 149 | 150 | // adding controls to the map 151 | 152 | map.setMaxBounds(bounds); 153 | map.panTo([cx,cy]); 154 | 155 | 156 | 157 | // adding names to the form 158 | 159 | document.getElementById("p1").innerHTML = "" + cities_select[0]["properties"]["NAME"] + ", " + cities_select[0]["properties"]["ADM0NAME"] 160 | document.getElementById("p2").innerHTML = "" + cities_select[1]["properties"]["NAME"] + ", " + cities_select[1]["properties"]["ADM0NAME"] 161 | document.getElementById("p3").innerHTML = "" + cities_select[2]["properties"]["NAME"] + ", " + cities_select[2]["properties"]["ADM0NAME"] 162 | document.getElementById("p4").innerHTML = "" + cities_select[3]["properties"]["NAME"] + ", " + cities_select[3]["properties"]["ADM0NAME"] 163 | 164 | }; 165 | 166 | 167 | 168 | 169 | // show the initial map 170 | 171 | showMap(clevel) 172 | 173 | 174 | 175 | 176 | // function for what happens when submit is clicked 177 | 178 | function submitAnswers() { 179 | 180 | // messages for when somone answeres correct 181 | 182 | var yesses = [ 183 | "Yes! Yes! Yes!", 184 | "Indeed", 185 | "Correct!", 186 | "Well Done!", 187 | "Ole!", 188 | ":)", 189 | "Perfecto", 190 | "Excellent!", 191 | "Wonderful!", 192 | "Fabulous!", 193 | "Hooray!", 194 | "Huzzah", 195 | "Top Notch", 196 | "Are you cheating?", 197 | "S M R T", 198 | "Woohoo!", 199 | "Amazing!", 200 | "Superstar!", 201 | "You're good!", 202 | "You got it!", 203 | "Great Job!", 204 | "Good!", 205 | "Great!", 206 | "Totally", 207 | "Tubular!" 208 | ] 209 | 210 | // grab the value of result (1 to 4) 211 | 212 | var q1 = document.forms['quizForm']['q1'].value; 213 | console.log(eval(q1)) 214 | 215 | // if correct 216 | 217 | if (eval(q1) === rando_i + 1) { 218 | console.log("correcto") 219 | //update the level 220 | clevel = clevel + 1 221 | // show the next map 222 | showMap(clevel) 223 | 224 | // update the level 225 | var count_score = parseInt(document.getElementById("level").innerHTML) + 1 226 | document.getElementById("level").innerHTML = count_score + "" 227 | 228 | // update the score 229 | score = p_score 230 | document.getElementById("score").innerHTML = score + "" 231 | 232 | // display a correct message 233 | document.getElementById("message").innerHTML = yesses[getRandomInt(0, yesses.length - 1)] 234 | 235 | } 236 | 237 | // if wrong 238 | 239 | else { 240 | 241 | console.log("boourns") 242 | 243 | // say game over 244 | document.getElementById("message").innerHTML = "GAME OVER :(

 The correct answer was " + w 245 | 246 | // assign high scores 247 | var escore = document.getElementById("hscore").innerHTML 248 | var elevel = document.getElementById("hlevel").innerHTML 249 | 250 | if (clevel > elevel) { 251 | document.getElementById("hlevel").innerHTML = (clevel - 1) + "" 252 | } 253 | 254 | if (score > escore) { 255 | document.getElementById("hscore").innerHTML = score + "" 256 | } 257 | 258 | // refresh scores and start the game over again 259 | clevel = 1 260 | remap = 1 261 | p_score = 0 262 | score = 0 263 | choiceCities = [] 264 | showMap(clevel) 265 | document.getElementById("score").innerHTML = "0" 266 | document.getElementById("level").innerHTML = "1" 267 | 268 | } 269 | 270 | return false; 271 | } 272 | --------------------------------------------------------------------------------