├── .gitignore ├── upvote.png ├── link-mode-dark.png ├── link-mode-light.png ├── LICENSE ├── README.md ├── style.css ├── src ├── Html.py └── main.py └── data └── cities /.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | .vscode 3 | data/historic 4 | __pycache__ 5 | -------------------------------------------------------------------------------- /upvote.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruarq/ukraine-war-heatmap/HEAD/upvote.png -------------------------------------------------------------------------------- /link-mode-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruarq/ukraine-war-heatmap/HEAD/link-mode-dark.png -------------------------------------------------------------------------------- /link-mode-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ruarq/ukraine-war-heatmap/HEAD/link-mode-light.png -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 ruarq 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 | # Ukraine War Map/Heatmap 2 | This project takes submissions about Ukraine from subreddits like r/CombatFootage, r/UkraineWarVideoReport, r/ukraine, r/worldnews, r/UkraineInvasionVideos and r/UkrainevRussia and translates them into a heatmap where most of the stuff related to the war in Ukraine is happening. 3 | You can see that heatmap [here](https://ruarq.github.io/ukraine-war-heatmap/). It updates every 30 minutes. 4 | 5 | ## It's over 6 | The heatmap is sadly not a good way of visualizing the war in ukraine, and activity on reddit is slowly dying. 7 | For these reasons I decided to shut the automatic updates down. 8 | 9 | ## Support Ukraine 10 | - Read [this](https://www.reddit.com/r/ukraine/comments/s6g5un/want_to_support_ukraine_heres_a_list_of_charities/) 11 | reddit post from [r/ukraine](https://www.reddit.com/r/ukraine) for a big list of charities to donate to 12 | - Do not spread misinformation and do report misinformation 13 | - You can help the [IT ARMY of Ukraine](https://t.me/itarmyofukraine2022) to fight against russia (at your own risk) 14 | - Checkout [all the other projects](https://github.com/topics/ukraine/) on github related to ukraine and the invasion 15 | 16 | ## How to run 17 | 1. Install dependencies with `pip install dotenv geopy praw` 18 | 19 | **NOTE:** You will also have to install my [folium fork](https://github.com/ruarq/folium). 20 | 21 | 2. Create `.env` file and populate it with: 22 | ```ini 23 | REDDIT_USERNAME= 24 | REDDIT_PASSWORD= 25 | REDDIT_API_ID= 26 | REDDIT_API_SECRECT= 27 | ``` 28 | 3. Create the empty directory `data/historic` 29 | 4. Run with `python src/main.py` 30 | -------------------------------------------------------------------------------- /style.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family: Arial, Helvetica, sans-serif; 3 | } 4 | 5 | a { 6 | font-weight: bold; 7 | text-decoration: none; 8 | color: black; 9 | } 10 | 11 | a:hover { 12 | text-decoration: underline; 13 | } 14 | 15 | .main-body { 16 | background: rgb(240, 240, 240); 17 | } 18 | 19 | .link-div { 20 | display: flex; 21 | align-items: center; 22 | } 23 | 24 | .link-img { 25 | height: 1em; 26 | } 27 | 28 | .score-div { 29 | color: gray; 30 | display: flex; 31 | align-items: center; 32 | } 33 | 34 | .upvote-img { 35 | height: 0.8em; 36 | position: relative; 37 | top: -0.5px; 38 | } 39 | /* 40 | .leaflet-map { 41 | float: left; 42 | width: 75%; 43 | height: 100%; 44 | } 45 | 46 | .news-column { 47 | float: right; 48 | width: 25%; 49 | height: 100%; 50 | overflow-x: hidden; 51 | overflow-y: auto; 52 | } 53 | */ 54 | .news-column-div { 55 | padding-top: 5px; 56 | padding-bottom: 5px; 57 | } 58 | 59 | .news-column-list { 60 | list-style-type: none; 61 | padding-left: 25px; 62 | padding-right: 25px; 63 | } 64 | 65 | .news-column-list li { 66 | margin-top: 10px; 67 | margin-bottom: 10px; 68 | } 69 | 70 | .element-content-div { 71 | padding: 10px; 72 | background: white; 73 | border-radius: 5px; 74 | border-bottom: 1px lightgrey solid; 75 | border-right: 1px lightgrey solid; 76 | } 77 | 78 | .element-info-div { 79 | display: flex; 80 | justify-content: space-between; 81 | margin-top: 7px; 82 | } 83 | 84 | .element-link-source { 85 | display: flex; 86 | justify-content: space-between; 87 | } 88 | 89 | .reddit-logo { 90 | display:flex; 91 | justify-content: center; 92 | align-items: center; 93 | background: white; 94 | margin: 10px 25px 10px 25px; 95 | border-radius: 5px; 96 | border-bottom: 1px lightgrey solid; 97 | border-right: 1px lightgrey solid; 98 | } 99 | 100 | .reddit-logo-img { 101 | height: 30px; 102 | } 103 | /* 104 | @media screen and (max-width: 1000px) { 105 | .leaflet-map { 106 | width: 100%; 107 | height: 75vh; 108 | } 109 | 110 | .news-column { 111 | width: 100%; 112 | height: 100%; 113 | overflow-x: visible; 114 | overflow-y: visible; 115 | } 116 | } 117 | */ 118 | -------------------------------------------------------------------------------- /src/Html.py: -------------------------------------------------------------------------------- 1 | # MIT License 2 | # 3 | # Copyright (c) 2022 ruarq 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 | 23 | 24 | class Element: 25 | def __init__(self, tag, attributes=dict()): 26 | self._tag = tag 27 | self.content = '' 28 | self._attributes = attributes 29 | self._has_closing_tag = True 30 | 31 | def __setitem__(self, key, value): 32 | self._attributes[key] = value 33 | 34 | def dumps(self): 35 | s = f'<{self._tag} ' 36 | for attribute in self._attributes: 37 | s += f'{attribute.lower()}="{self._attributes[attribute]}" ' 38 | 39 | if self._has_closing_tag: 40 | s += '>' 41 | if type(self.content) is list: 42 | for elem in self.content: 43 | s += str(elem) 44 | elif self.content is not None: 45 | s += str(self.content) 46 | s += f'' 47 | else: 48 | s += '/>' 49 | 50 | return s 51 | 52 | def __str__(self): 53 | return self.dumps() 54 | 55 | class Html(Element): 56 | def __init__(self, content=None): 57 | super().__init__('html') 58 | self.content = content 59 | 60 | class Head(Element): 61 | def __init__(self, content=None): 62 | super().__init__('head') 63 | self.content = content 64 | 65 | class Meta(Element): 66 | def __init__(self, **attributes): 67 | super().__init__('meta', attributes) 68 | self._has_closing_tag = False 69 | 70 | class Link(Element): 71 | def __init__(self, **attributes): 72 | super().__init__('link', attributes) 73 | self._has_closing_tag = False 74 | 75 | class Title(Element): 76 | def __init__(self, content=None): 77 | super().__init__('title') 78 | self.content = content 79 | 80 | class Body(Element): 81 | def __init__(self, content=None, **attributes): 82 | super().__init__('body', attributes) 83 | self.content = content 84 | 85 | class Div(Element): 86 | def __init__(self, content=None, **attributes): 87 | super().__init__('div', attributes) 88 | self.content = content 89 | 90 | class H2(Element): 91 | def __init__(self, content=None, **attributes): 92 | super().__init__('h2', attributes) 93 | self.content = content 94 | 95 | class Img(Element): 96 | def __init__(self, **attributes): 97 | super().__init__('img', attributes) 98 | self._has_closing_tag = False 99 | 100 | class A(Element): 101 | def __init__(self, content=None, **attributes): 102 | super().__init__('a', attributes) 103 | self.content = content 104 | 105 | class Ul(Element): 106 | def __init__(self, content=None, **attributes): 107 | super().__init__('ul', attributes) 108 | self.content = content 109 | 110 | class Li(Element): 111 | def __init__(self, content=None, **attributes): 112 | super().__init__('li', attributes) 113 | self.content = content 114 | 115 | class Span(Element): 116 | def __init__(self, content=None, **attributes): 117 | super().__init__('span', attributes) 118 | self.content = content 119 | 120 | class Picture(Element): 121 | def __init__(self, content=None, **attributes): 122 | super().__init__('picture', attributes) 123 | self.content = content 124 | 125 | class Source(Element): 126 | def __init__(self, **attributes): 127 | super().__init__('source', attributes) 128 | self._has_closing_tag = False 129 | -------------------------------------------------------------------------------- /src/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # MIT License 4 | # 5 | # Copyright (c) 2022 ruarq 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | import praw 26 | from geopy.geocoders import Nominatim 27 | import folium as fl 28 | from folium.plugins import HeatMapWithTime, HeatMap 29 | import json 30 | from datetime import datetime 31 | import os 32 | import math 33 | from dotenv import load_dotenv 34 | import Html 35 | from uuid import uuid1 36 | from functools import cmp_to_key 37 | 38 | TIME_FORMAT = '%Y-%m-%d %H:%M UTC' 39 | SUBMISSION_QUERY_LIMIT = 300 40 | 41 | submission_count = 0 42 | 43 | class RedditNews: 44 | def __init__(self, title, score, source): 45 | self.title = title 46 | self.score = score 47 | self.source = source 48 | 49 | def load_cities(): 50 | cities = open(f'data/cities').read().split('\n') 51 | # Remove empty city names 52 | return [city for city in cities if len(city) > 1] 53 | 54 | def query_submissions(reddit, sub): 55 | print(f"Querying hot submissions from 'r/{sub}'") 56 | r = reddit.subreddit(sub).hot(limit=SUBMISSION_QUERY_LIMIT) 57 | return r 58 | 59 | def permalink_to_url(permalink): 60 | return 'https://www.reddit.com' + permalink 61 | 62 | def query_news(reddit, source='worldnews'): 63 | submissions = query_submissions(reddit, source) 64 | 65 | # Words to look out for in the submission titles. NOTE: should be all lowercase 66 | trigger_words = ['ukraine', 'russia', 'zelensky', 'putin', 'belarus'] 67 | 68 | news = list() 69 | 70 | for submission in submissions: 71 | # no self posts, only links to real news articles 72 | is_self_post = submission.url == permalink_to_url(submission.permalink) 73 | if submission.score < 10000 or is_self_post: 74 | continue 75 | 76 | # Check all the trigger words 77 | relevant = False 78 | for trigger_word in trigger_words: 79 | if trigger_word in submission.title.lower(): 80 | relevant = True 81 | 82 | if relevant: 83 | news.append( 84 | RedditNews( 85 | submission.title, 86 | submission.score, 87 | submission.url 88 | ) 89 | ) 90 | 91 | return news 92 | 93 | def get_city_mention_counts(cities, submissions): 94 | mentions = dict() 95 | 96 | # search in every submission 97 | for submission in submissions: 98 | global submission_count 99 | submission_count += 1 100 | 101 | # filter 'bad' submissions 102 | if submission.score < 100: 103 | continue 104 | 105 | # go through each city and check if it's in the submission title 106 | for city in cities: 107 | found = False 108 | 109 | # some cities are made up of multiple words, check the words too 110 | # for word in city.split(' '): 111 | # if word.lower() in submission.title.lower(): 112 | # found = True 113 | 114 | # check the whole city name 115 | if city.lower() in submission.title.lower(): 116 | found = True 117 | 118 | # add to mentions if the city is in the submission title 119 | if found: 120 | # print(f'\t{city}\n') 121 | if city in mentions: 122 | mentions[city] += 1 123 | else: 124 | mentions[city] = 1 125 | 126 | return mentions 127 | 128 | def query_city_location(city): 129 | s = f"Querying location for '{city}'" 130 | 131 | # Nominatim can't find the location for some cities, but we know they exists so translate them 132 | # to something Nominatim can find 133 | translations = { 134 | 'borodyanka': 'borodianka' 135 | } 136 | 137 | if city.lower() in translations: 138 | city = translations[city.lower()] 139 | 140 | try: 141 | nominatim = Nominatim(user_agent='ukraine-war-heatmap') 142 | location = nominatim.geocode(city, country_codes='ua') 143 | location = [float(location.raw['lat']), float(location.raw['lon'])] 144 | 145 | print(f'[OKAY] {s}') 146 | return location 147 | 148 | except: # TODO(ruarq): find out what exception Nominatim throws 149 | print(f'[FAIL] {s}') 150 | return None 151 | 152 | def generate_heatmap(mapdata): 153 | map = fl.Map( 154 | tiles='Stamen Terrain', 155 | location=[49.3956617, 30.9809839], zoom_start=6 156 | ) 157 | 158 | # timestamps, sort ascending 159 | index = list(mapdata.keys()) 160 | 161 | # convert to datetime object (so they get sorted correctly) 162 | for i in range(len(index)): 163 | index[i] = datetime.strptime(index[i], TIME_FORMAT) 164 | 165 | # sort 166 | index.sort() 167 | 168 | # convert back to string 169 | for i in range(len(index)): 170 | index[i] = str(index[i].strftime(TIME_FORMAT)) 171 | 172 | HeatMapWithTime( 173 | name='Heatmap', 174 | data=list(mapdata.values()), 175 | index=index, 176 | radius=0.5, 177 | scale_radius=True, 178 | start_index=len(index) 179 | ).add_to(map) 180 | 181 | fl.TileLayer('OpenStreetMap').add_to(map) 182 | fl.LayerControl().add_to(map) 183 | 184 | # Yes. I know. We have to do it like that. 185 | # map._repr_html_() will produce a stupid bug 186 | # where the map keeps reloading in itself. 187 | filename = 'map_' + str(uuid1()) + '.html' 188 | map.save(filename) 189 | file = open(filename, 'r') 190 | content = file.read() 191 | file.close() 192 | os.remove(filename) 193 | return content 194 | 195 | def merge_dicts(a, b): 196 | return { k: a.get(k, 0) + b.get(k, 0) for k in set(a) | set(b) } 197 | 198 | def query_mentions_from_subreddits(reddit, cities, subreddits): 199 | mentions = dict() 200 | for subreddit in subreddits: 201 | try: 202 | submissions = query_submissions(reddit, subreddit) 203 | mentions = merge_dicts(mentions, get_city_mention_counts(cities, submissions)) 204 | except: 205 | print(f'Something bad happened while querying mentions from r/{subreddit}') 206 | 207 | return mentions 208 | 209 | def load_historic_data(directory): 210 | data = {} 211 | for file in os.listdir(directory): 212 | f = os.path.join(directory, file) 213 | if os.path.isfile(f): 214 | data[file] = json.load(open(f, 'r')) 215 | 216 | return data 217 | 218 | def generate_news_column(reddit): 219 | subreddit = 'worldnews' 220 | news = query_news(reddit, subreddit) 221 | 222 | html_news_list = list() 223 | 224 | # sort news by upvotes, most upvotes should be first 225 | news.sort(key=cmp_to_key(lambda a, b : b.score - a.score)) 226 | 227 | for n in news: 228 | html_news_list.append( 229 | Html.Li( 230 | Html.Div( 231 | content=[ 232 | Html.Div(n.title), 233 | Html.Div( 234 | [ 235 | Html.A( 236 | content=[ 237 | Html.Div( 238 | [ 239 | Html.Img( 240 | src='link-mode-light.png', 241 | alt='hyperlink image', 242 | Class='link-img' 243 | ), 244 | 'Source' 245 | ], 246 | Class='link-div' 247 | ) 248 | ], 249 | href=n.source 250 | ), 251 | Html.Div( 252 | [ 253 | Html.Img(src='upvote.png', alt='upvote', Class='upvote-img'), 254 | f' {int((n.score / 1000) + 0.5)}K' 255 | ], 256 | Class='score-div' 257 | ) 258 | ], 259 | Class='element-info-div' 260 | ) 261 | ], 262 | Class='element-content-div' 263 | ) 264 | ) 265 | ) 266 | 267 | return Html.Div( 268 | Html.Div( 269 | [ 270 | Html.Div( 271 | content=[ 272 | Html.Img( 273 | src='https://www.redditinc.com/assets/images/site/reddit-logo.png', 274 | alt='reddit logo', 275 | Class='reddit-logo-img' 276 | ), 277 | Html.H2( 278 | [ 279 | ' ', 280 | Html.A(content=f'r/{subreddit}', 281 | href=f'https://www.reddit.com/r/{subreddit}' 282 | ) 283 | ] 284 | ) 285 | ], 286 | Class='reddit-logo' 287 | ), 288 | Html.Ul( 289 | content=html_news_list, 290 | Class='news-column-list' 291 | ) 292 | ], 293 | Class='news-column-div' 294 | ), 295 | Class='news-column' 296 | ) 297 | 298 | def main(): 299 | load_dotenv() 300 | 301 | now = datetime.utcnow().strftime(TIME_FORMAT) 302 | 303 | # init praw 304 | reddit = praw.Reddit( 305 | client_id=os.getenv('REDDIT_API_ID'), 306 | client_secret=os.getenv('REDDIT_API_SECRET'), 307 | username=os.getenv('REDDIT_USERNAME'), 308 | password=os.getenv('REDDIT_PASSWORD'), 309 | user_agent='ukraine war hotmap' 310 | ) 311 | 312 | # query mentions of cities 313 | cities = load_cities() 314 | mentions = query_mentions_from_subreddits( 315 | reddit, 316 | cities, 317 | [ 318 | 'CombatFootage', 319 | 'UkraineWarVideoReport', 320 | 'ukraine', 321 | 'worldnews', 322 | 'UkrainevRussia', 323 | 'UkraineInvasionVideos', 324 | 'UkrainianConflict', 325 | 'Ukraine_UA', 326 | 'UkraineWarReports' 327 | ] 328 | ) 329 | 330 | # dump for historic data and heatmap with time 331 | json.dump(mentions, open(f'data/historic/{now}', 'w')) 332 | 333 | # print debug info 334 | print(f'Searched in {submission_count} submissions and found:') 335 | print(json.dumps(mentions, indent=4, sort_keys=True)) 336 | 337 | mapdata = {} 338 | locations = {} 339 | data = load_historic_data('data/historic') 340 | 341 | # smooth out the values, since reddit data is a little bit inconsistent 342 | prev_time = None 343 | smooth_data = dict() 344 | for time in data.keys(): 345 | smooth_data[time] = dict() 346 | for city in data[time].keys(): 347 | if prev_time is not None: 348 | value = data[time][city] 349 | prev_value = 0 350 | if city in smooth_data[prev_time]: 351 | prev_value = smooth_data[prev_time][city] 352 | delta = value - prev_value 353 | smooth_data[time][city] = prev_value + delta * 0.25 354 | else: 355 | smooth_data[time][city] = data[time][city] 356 | prev_time = time 357 | data = smooth_data 358 | 359 | # compile mapdata for folium 360 | for time in data.keys(): 361 | # get the city locations for each city 362 | for city in data[time].keys(): 363 | l = 0 364 | 365 | # check if the city location was already queried (we want to minimize nominatim api requests) 366 | if city in locations: 367 | l = locations[city] 368 | else: 369 | l = query_city_location(city) 370 | 371 | locations[city] = l 372 | 373 | # add location to the mapdata if queried successfully 374 | if l is not None: 375 | d = [ 376 | round(l[0], 2), 377 | round(l[1], 2), 378 | round(math.sqrt(data[time][city] / max(data[time].values())), 2) 379 | ] 380 | if time in mapdata: 381 | mapdata[time].append(d) 382 | else: 383 | mapdata[time] = [d] 384 | 385 | heatmap_html = generate_heatmap(mapdata) 386 | 387 | file = open('index.html', 'w') 388 | file.write( 389 | Html.Html( 390 | [ 391 | Html.Head( 392 | [ 393 | Html.Meta(charset='utf-8'), 394 | Html.Meta(name='description', content='Watch a live heatmap to see where the war in ukraine is most active.'), 395 | Html.Meta(name='author', content='ruarq'), 396 | Html.Meta(name='copyright', content='ruarq'), 397 | Html.Meta(name='language', content='English'), 398 | Html.Meta(name='keywords', content='ukraine, ukraine invasion, ukraine heatmap, ukraine live heatmap, map, heatmap, ukraine map, ukraine live map, live map, help ukraine, ruarq, github.io, ukraine conflict, conflict, ukraine war, war, russia, ukraine russia, ukraine russia war'), 399 | Html.Meta(name='revisit-after', content='1 days'), 400 | Html.Title('Ukraine War Map/Heatmap by ruarq'), 401 | Html.Link(rel='stylesheet', type='text/css', href='style.css') 402 | ] 403 | ), 404 | Html.Body( 405 | [ 406 | Html.Div( 407 | content=heatmap_html, 408 | Class='leaflet-map' 409 | ), 410 | generate_news_column(reddit) 411 | ], 412 | Class='main-body' 413 | ) 414 | ] 415 | ).dumps() 416 | ) 417 | 418 | if __name__ == '__main__': 419 | main() 420 | -------------------------------------------------------------------------------- /data/cities: -------------------------------------------------------------------------------- 1 | Alchevsk 2 | Almazna 3 | Alupka 4 | Alushta 5 | Amvrosiivka 6 | Ananiv 7 | Andrushivka 8 | Antratsyt 9 | Apostolove 10 | Armiansk 11 | Artemivsk 12 | Artemove 13 | Artsyz 14 | Avdiivka 15 | Bakhchisaray 16 | Bakhmach 17 | Bakhmut 18 | Balakliia 19 | Balta 20 | Bar 21 | Baranivka 22 | Barvinkove 23 | Bashtanka 24 | Baturyn 25 | Belz 26 | Berdiansk 27 | Berdychiv 28 | Berehove 29 | Berestechko 30 | Berezan 31 | Berezhany 32 | Berezivka 33 | Berezne 34 | Bershad 35 | Beryslav 36 | Bibrka 37 | Bila Tserkva 38 | Bilhorod-Dnistrovskyi 39 | Biliaivka 40 | Bilohirsk 41 | Bilopillia 42 | Bilozerske 43 | Bilytske 44 | Blahovishchenske 45 | Bobrovytsia 46 | Bobrynets 47 | Bohodukhiv 48 | Bohuslav 49 | Boiarka 50 | Bolekhiv 51 | Bolhrad 52 | Borodyanka 53 | Borschiv 54 | Boryslav 55 | Boryspil 56 | Borzna 57 | Brianka 58 | Brody 59 | Broshniv-Osada 60 | Brovary 61 | Bucha 62 | Buchach 63 | Burshtyn 64 | Buryn 65 | Busk 66 | Charkiv 67 | Chasiv Yar 68 | Cherkasy 69 | Cherkasy Oblast 70 | Chernihiv 71 | Chernihiv Oblast 72 | Chernivtsi 73 | Chernivtsi Oblast 74 | Chervonohrad 75 | Chervonopartyzansk 76 | Chop 77 | Chornobyl 78 | Chornomorsk 79 | Chortkiv 80 | Chuhuiv 81 | Chyhyryn 82 | Chystiakove 83 | Crimea 84 | Debaltseve 85 | Derazhnia 86 | Derhachi 87 | Dnipro 88 | Dnipropetrovsk Oblast 89 | Dniprorudne 90 | Dobromyl 91 | Dobropillia 92 | Dokuchaievsk 93 | Dolyna 94 | Dolynska 95 | Donetsk 96 | Donetsk Oblast 97 | Dovzhansk 98 | Drohobych 99 | Druzhba 100 | Druzhkivka 101 | Dubliany 102 | Dubno 103 | Dubrovytsia 104 | Dunaivtsi 105 | Dzhankoy 106 | Enerhodar 107 | Fastiv 108 | Feodosiya 109 | Hadiach 110 | Haisyn 111 | Haivoron 112 | Halych 113 | Henichesk 114 | Hertsa 115 | Hirnyk 116 | Hirske 117 | Hlobyne 118 | Hlukhiv 119 | Hlyniany 120 | Hnivan 121 | Hola Prystan 122 | Holubivka 123 | Horishni Plavni 124 | Horlivka 125 | Horodenka 126 | Horodnia 127 | Horodok 128 | Horodysche 129 | Horokhiv 130 | Hostomel 131 | Hrebinka 132 | Huliaipole 133 | Ichnia 134 | Illintsi 135 | Ilovaisk 136 | Inkerman 137 | Irpin 138 | Irshava 139 | Ivankov 140 | Ivano-Frankivsk 141 | Ivano-Frankivsk Oblast 142 | Iziaslav 143 | Izium 144 | Izmail 145 | Kadiyivka 146 | Kaharlyk 147 | Kakhovka 148 | Kalush 149 | Kalynivka 150 | Kamianets-Podilskyi 151 | Kamianka 152 | Kamianka-Buzka 153 | Kamianka-Dniprovska 154 | Kamianske 155 | Kamin-Kashyrskyi 156 | Kaniv 157 | Karlivka 158 | Kerch 159 | Kharkiv 160 | Kharkiv Oblast 161 | Khartsyzk 162 | Kherson 163 | Kherson Oblast 164 | Khmelnytskyi 165 | Khmelnytskyi Oblast 166 | Khmilnyk 167 | Khodoriv 168 | Khorol 169 | Khorostkiv 170 | Khotyn 171 | Khrestivka 172 | Khrustalnyi 173 | Khrystynivka 174 | Khust 175 | Khyriv 176 | Kiev 177 | Kilia 178 | Kirovohrad Oblast 179 | Kitsman 180 | Kivertsi 181 | Kobeliaky 182 | Kodyma 183 | Kolomyia 184 | Komarno 185 | Komsomolske 186 | Konotop 187 | Kopychyntsi 188 | Korets 189 | Koriukivka 190 | Korosten 191 | Korostyshiv 192 | Korsun-Shevchenkivskyi 193 | Kosiv 194 | Kostiantynivka 195 | Kostopil 196 | Kovel 197 | Koziatyn 198 | Kramatorsk 199 | Krasnohorivka 200 | Krasnohrad 201 | Krasnoperekopsk 202 | Krasyliv 203 | Kremenchuk 204 | Kremenets 205 | Kreminna 206 | Krolevets 207 | Kropyvnytskyi 208 | Kryvyi 209 | Kryvyi Rih 210 | Kupiansk 211 | Kurakhove 212 | Kyiv 213 | Kyiv Oblast 214 | Ladyzhyn 215 | Lanivtsi 216 | Lebedyn 217 | Liuboml 218 | Liubotyn 219 | Lokhvytsia 220 | Lozova 221 | Lubny 222 | Lugansk 223 | Luhansk 224 | Luhansk Oblast 225 | Lutsk 226 | Lutuhyne 227 | Lviv 228 | Lviv Oblast 229 | Lyman 230 | Lypovets 231 | Lysychansk 232 | Makiivka 233 | Mala Vyska 234 | Malyn 235 | Marhanets 236 | Mariupol 237 | Maryinka 238 | Maydanets 239 | Melitopol 240 | Mena 241 | Merefa 242 | Miusynsk 243 | Mohyliv-Podilskyi 244 | Molochansk 245 | Molodohvardiysk 246 | Monastyrysche 247 | Monastyryska 248 | Morshyn 249 | Mospyne 250 | Mostyska 251 | Mukachevo 252 | Mykolaiv 253 | Mykolaivka 254 | Mykolaiv Oblast 255 | Myrhorod 256 | Myrnohrad 257 | Myronivka 258 | Nadvirna 259 | Nemyriv 260 | Netishyn 261 | Nikopol 262 | Nizhyn 263 | Nosivka 264 | Nova Kakhovka 265 | Nova Odesa 266 | Novhorod-Siverskyi 267 | Novoazovsk 268 | Novodnistrovsk 269 | Novodruzhesk 270 | Novohrad-Volynskyi 271 | Novohrodivka 272 | Novoiavorivsk 273 | Novomoskovsk 274 | Novomyrhorod 275 | Novoselytsia 276 | Novoukrainka 277 | Novovolynsk 278 | Novyi Buh 279 | Novyi Kalyniv 280 | Novyi Rozdil 281 | Obukhiv 282 | Ochakiv 283 | Odesa 284 | Odesa Oblast 285 | Okhtyrka 286 | Oleksandriia 287 | Oleksandrivsk 288 | Oleshky 289 | Olevsk 290 | Orikhiv 291 | Oster 292 | Ostroh 293 | Ovruch 294 | Pavlohrad 295 | Perechyn 296 | Pereiaslav 297 | Peremyshliany 298 | Pereschepyne 299 | Perevalsk 300 | Pershotravensk 301 | Pervomaisk 302 | Pervomaiskyi 303 | Petrovske 304 | Piatykhatky 305 | Pidhaitsi 306 | Pidhorodne 307 | Pivdenne 308 | Pochaiv 309 | Podilsk 310 | Pohrebysche 311 | Pokrov 312 | Pokrovsk 313 | Polohy 314 | Polonne 315 | Poltava 316 | Poltava Oblast 317 | Pomichna 318 | Popasna 319 | Pryluky 320 | Prymorsk 321 | Prypiat 322 | Pryvillia 323 | Pustomyty 324 | Putyvl 325 | Pyriatyn 326 | Radekhiv 327 | Radomyshl 328 | Radyvyliv 329 | Rakhiv 330 | Rava-Ruska 331 | Reni 332 | Rivne 333 | Rivne Oblast 334 | Rodynske 335 | Rohatyn 336 | Romny 337 | Rovenky 338 | Rozdilna 339 | Rozhysche 340 | Rubizhne 341 | Rudky 342 | Rzhyschiv 343 | Saky 344 | Sambir 345 | Sarny 346 | Selydove 347 | Semenivka 348 | Seredyna-Buda 349 | Sevastopol 350 | Shakhtarsk 351 | Sharhorod 352 | Shchastia 353 | Shcholkine 354 | Shepetivka 355 | Shostka 356 | Shpola 357 | Shumsk 358 | Sieverodonetsk 359 | Simferopol 360 | Siversk 361 | Skadovsk 362 | Skalat 363 | Skole 364 | Skvyra 365 | Slavuta 366 | Slavutych 367 | Sloviansk 368 | Smila 369 | Sniatyn 370 | Snihurivka 371 | Snizhne 372 | Snovsk 373 | Sokal 374 | Sokyriany 375 | Soledar 376 | Sorokyne 377 | Sosnivka 378 | Starobilsk 379 | Starokostiantyniv 380 | Staryi Krym 381 | Staryi Sambir 382 | Stebnyk 383 | Storozhynets 384 | Stryi 385 | Sudak 386 | Sudova Vyshnia 387 | Sukhodilsk 388 | Sumy 389 | Sumy Oblast 390 | Svaliava 391 | Svarychiv 392 | Svatove 393 | Sviatohirsk 394 | Svitlodarsk 395 | Svitlovodsk 396 | Synelnykove 397 | Talne 398 | Tarascha 399 | Tatarbunary 400 | Tavriysk 401 | Teplodar 402 | Teplohirsk 403 | Terebovlia 404 | Ternivka 405 | Ternopil 406 | Ternopil Oblast 407 | Tetiiv 408 | Tiachiv 409 | Tlumach 410 | Tokmak 411 | Toretsk 412 | Trostianets 413 | Trostyanets 414 | Truskavets 415 | Tulchyn 416 | Turka 417 | Tysmenytsia 418 | Uhniv 419 | Ukrainka 420 | Ukrainsk 421 | Uman 422 | Ustyluh 423 | Uzhhorod 424 | Uzyn 425 | Vakhrusheve 426 | Valky 427 | Varash 428 | Vashkivtsi 429 | Vasylivka 430 | Vasylkiv 431 | Vatutine 432 | Velyki Mosty 433 | Verkhivtseve 434 | Verkhniodniprovsk 435 | Vilniansk 436 | Vilnohirsk 437 | Vinnytsia 438 | Vinnytsia Oblast 439 | Volnovakha 440 | Volochysk 441 | Volodymyr-Volynskyi 442 | Volyn Oblast 443 | Vorozhba 444 | Vovchansk 445 | Voznesensk 446 | Vuhledar 447 | Vuhlehirsk 448 | Vylkove 449 | Vynnyky 450 | Vynohradiv 451 | Vyshhorod 452 | Vyshneve 453 | Vyzhnytsia 454 | Yahotyn 455 | Yalta 456 | Yampil 457 | Yaremcha 458 | Yasynuvata 459 | Yavoriv 460 | Yenakiieve 461 | Yevpatoria 462 | Yunokomunarivsk 463 | Yuzhne 464 | Yuzhnoukrainsk 465 | Zakarpattia Oblast 466 | Zalischyky 467 | Zaporizhzhia 468 | Zaporizhzhia Oblast 469 | Zaporozhye 470 | Zastavna 471 | Zavodske 472 | Zbarazh 473 | Zboriv 474 | Zdolbuniv 475 | Zelenodolsk 476 | Zhashkiv 477 | Zhdanivka 478 | Zhmerynka 479 | Zhovkva 480 | Zhovti Vody 481 | Zhydachiv 482 | Zhytomyr 483 | Zhytomyr Oblast 484 | Zinkiv 485 | Zmiiv 486 | Znamianka 487 | Zolochiv 488 | Zolote 489 | Zolotonosha 490 | Zorynsk 491 | Zuhres 492 | Zvenyhorodka 493 | Zymohiria 494 | Авдіївка 495 | Алмазна 496 | Алупка 497 | Алушта 498 | Алчевськ 499 | Амвросіївка 500 | Ананьїв 501 | Андрушівка 502 | Антрацит 503 | Апостолове 504 | Армянськ 505 | Артемівськ 506 | Артемове 507 | Арциз 508 | Балаклія 509 | Балта 510 | Бар 511 | Баранівка 512 | Барвінкове 513 | Батурин 514 | Бахмач 515 | Бахмут 516 | Бахчисарай 517 | Баштанка 518 | Белз 519 | Бердичів 520 | Бердянськ 521 | Берегове 522 | Бережани 523 | Березань 524 | Березівка 525 | Березне 526 | Берестечко 527 | Берислав 528 | Бершадь 529 | Бібрка 530 | Біла Церква 531 | Білгород-Дністровський 532 | Білицьке 533 | Білогірськ 534 | Білозерське 535 | Білопілля 536 | Біляївка 537 | Благовіщенське 538 | Бобринець 539 | Бобровиця 540 | Богодухів 541 | Богуслав 542 | Болград 543 | Болехів 544 | Борзна 545 | Борислав 546 | Бориспіль 547 | Борщів 548 | Боярка 549 | Бровари 550 | Броди 551 | Брошнів-Осада 552 | Брянка 553 | Буринь 554 | Бурштин 555 | Буськ 556 | Буча 557 | Бучач 558 | Валки 559 | Вараш 560 | Василівка 561 | Васильків 562 | Ватутіне 563 | Вахрушеве 564 | Вашківці 565 | Великі Мости 566 | Верхівцеве 567 | Верхньодніпровськ 568 | Вижниця 569 | Вилкове 570 | Винники 571 | Виноградів 572 | Вишгород 573 | Вишневе 574 | Вільногірськ 575 | Вільнянськ 576 | Вінниця 577 | Вовчанськ 578 | Вознесенськ 579 | Волноваха 580 | Володимир-Волинський 581 | Волочиськ 582 | Ворожба 583 | Вуглегірськ 584 | Вугледар 585 | Гадяч 586 | Гайворон 587 | Гайсин 588 | Галич 589 | Генічеськ 590 | Герца 591 | Гірник 592 | Гірське 593 | Глиняни 594 | Глобине 595 | Глухів 596 | Гнівань 597 | Гола Пристань 598 | Голубівка 599 | Горішні Плавні 600 | Горлівка 601 | Городенка 602 | Городище 603 | Городня 604 | Городок 605 | Горохів 606 | Гребінка 607 | Гуляйполе 608 | Дебальцеве 609 | Деражня 610 | Дергачі 611 | Джанкой 612 | Дніпро 613 | Дніпрорудне 614 | Добромиль 615 | Добропілля 616 | Довжанськ 617 | Докучаєвськ 618 | Долина 619 | Долинська 620 | Донецьк 621 | Дрогобич 622 | Дружба 623 | Дружківка 624 | Дубляни 625 | Дубно 626 | Дубровиця 627 | Дунаївці 628 | Енергодар 629 | Євпаторія 630 | Єнакієве 631 | Жашків 632 | Жданівка 633 | Жидачів 634 | Житомир 635 | Жмеринка 636 | Жовква 637 | Жовті Води 638 | Заводське 639 | Заліщики 640 | Запоріжжя 641 | Заставна 642 | Збараж 643 | Зборів 644 | Звенигородка 645 | Здолбунів 646 | Зеленодольськ 647 | Зимогір'я 648 | Зіньків 649 | Зміїв 650 | Знам'янка 651 | Золоте 652 | Золотоноша 653 | Золочів 654 | Зоринськ 655 | Зугрес 656 | Івано-Франківськ 657 | Ізмаїл 658 | Ізюм 659 | Ізяслав 660 | Іллінці 661 | Іловайськ 662 | Інкерман 663 | Ірпінь 664 | Іршава 665 | Ічня 666 | Кагарлик 667 | Кадіївка 668 | Калинівка 669 | Калуш 670 | Камінь-Каширський 671 | Кам'янець-Подільський 672 | Кам'янка 673 | Кам'янка-Бузька 674 | Кам'янка-Дніпровська 675 | Кам'янське 676 | Канів 677 | Карлівка 678 | Каховка 679 | Керч 680 | Київ 681 | Ківерці 682 | Кілія 683 | Кіцмань 684 | Кобеляки 685 | Ковель 686 | Кодима 687 | Козятин 688 | Коломия 689 | Комарно 690 | Комсомольське 691 | Конотоп 692 | Копичинці 693 | Корець 694 | Коростень 695 | Коростишів 696 | Корсунь-Шевченківський 697 | Корюківка 698 | Косів 699 | Костопіль 700 | Костянтинівка 701 | Краматорськ 702 | Красилів 703 | Красногорівка 704 | Красноград 705 | Красноперекопськ 706 | Кременець 707 | Кременчук 708 | Кремінна 709 | Кривий Ріг 710 | Кролевець 711 | Кропивницький 712 | Куп'янськ 713 | Курахове 714 | Ладижин 715 | Ланівці 716 | Лебедин 717 | Лиман 718 | Липовець 719 | Лисичанськ 720 | Лозова 721 | Лохвиця 722 | Лубни 723 | Луганськ 724 | Лутугине 725 | Луцьк 726 | Львів 727 | Любомль 728 | Люботин 729 | Майданец 730 | Макіївка 731 | Мала Виска 732 | Малин 733 | Марганець 734 | Мар'їнка 735 | Маріуполь 736 | Мелітополь 737 | Мена 738 | Мерефа 739 | Миколаїв 740 | Миколаївка 741 | Миргород 742 | Мирноград 743 | Миронівка 744 | Міусинськ 745 | Могилів-Подільський 746 | Молодогвардійськ 747 | Молочанськ 748 | Монастириська 749 | Монастирище 750 | Моршин 751 | Моспине 752 | Мостиська 753 | Мукачево 754 | Надвірна 755 | Немирів 756 | Нетішин 757 | Ніжин 758 | Нікополь 759 | Нова Каховка 760 | Нова Одеса 761 | Новгород-Сіверський 762 | Новий Буг 763 | Новий Калинів 764 | Новий розділ 765 | Новоазовськ 766 | Нововолинськ 767 | Новоград-Волинський 768 | Новогродівка 769 | Новодністровськ 770 | Новодружеськ 771 | Новомиргород 772 | Новомосковськ 773 | Новоселиця 774 | Новоукраїнка 775 | Новояворівськ 776 | Носівка 777 | Обухів 778 | Овруч 779 | Одеса 780 | Олевськ 781 | Олександрівськ 782 | Олександрія 783 | Олешки 784 | Оріхів 785 | Остер 786 | Острог 787 | Охтирка 788 | Очаків 789 | Павлоград 790 | Первомайськ 791 | Первомайський 792 | Перевальськ 793 | Перемишляни 794 | Перечин 795 | Перещепине 796 | Переяслав 797 | Першотравенськ 798 | Петровське 799 | Пирятин 800 | Південне 801 | Підгайці 802 | Підгородне 803 | Погребище 804 | Подільськ 805 | Покров 806 | Покровськ 807 | Пологи 808 | Полонне 809 | Полтава 810 | Помічна 811 | Попасна 812 | Почаїв 813 | Привілля 814 | Прилуки 815 | Приморськ 816 | Прип'ять 817 | Пустомити 818 | Путивль 819 | П'ятихатки 820 | Рава-руська 821 | Радехів 822 | Радивилів 823 | Радомишль 824 | Рахів 825 | Рені 826 | Ржищів 827 | Рівне 828 | Ровеньки 829 | Рогатин 830 | Родинське 831 | Рожище 832 | Роздільна 833 | Ромни 834 | Рубіжне 835 | Рудки 836 | Саки 837 | Самбір 838 | Сарни 839 | Свалява 840 | Сваричів 841 | Сватове 842 | Світловодськ 843 | Світлодарськ 844 | Святогірськ 845 | Севастополь 846 | Селидове 847 | Семенівка 848 | Середина-Буда 849 | Сєверодонецьк 850 | Синельникове 851 | Сіверськ 852 | Сімферополь 853 | Скадовськ 854 | Скалат 855 | Сквира 856 | Сколе 857 | Славута 858 | Славутич 859 | Слов'янськ 860 | Сміла 861 | Снігурівка 862 | Сніжне 863 | Сновськ 864 | Снятин 865 | Сокаль 866 | Сокиряни 867 | Соледар 868 | Сорокине 869 | Соснівка 870 | Старий Крим 871 | Старий Самбір 872 | Старобільськ 873 | Старокостянтинів 874 | Стебник 875 | Сторожинець 876 | Стрий 877 | Судак 878 | Судова Вишня 879 | Суми 880 | Суходільськ 881 | Таврійськ 882 | Тальне 883 | Тараща 884 | Татарбунари 885 | Теплогірськ 886 | Теплодар 887 | Теребовля 888 | Тернівка 889 | Тернопіль 890 | Тетіїв 891 | Тисмениця 892 | Тлумач 893 | Токмак 894 | Торецьк 895 | Тростянець 896 | Трускавець 897 | Тульчин 898 | Турка 899 | Тячів 900 | Угнів 901 | Ужгород 902 | Узин 903 | Українка 904 | Українськ 905 | Умань 906 | Устилуг 907 | Фастів 908 | Феодосія 909 | Харків 910 | Харцизьк 911 | Херсон 912 | Хирів 913 | Хмельницький 914 | Хмільник 915 | Ходорів 916 | Хорол 917 | Хоростків 918 | Хотин 919 | Хрестівка 920 | Христинівка 921 | Хрустальний 922 | Хуст 923 | Часів Яр 924 | Червоноград 925 | Червонопартизанськ 926 | Черкаси 927 | Чернівці 928 | Чернігів 929 | Чигирин 930 | Чистякове 931 | Чоп 932 | Чорнобиль 933 | Чорноморськ 934 | Чортків 935 | Чугуїв 936 | Шаргород 937 | Шахтарськ 938 | Шепетівка 939 | Шостка 940 | Шпола 941 | Шумськ 942 | Щастя 943 | Щолкіне 944 | Южне 945 | Южноукраїнськ 946 | Юнокомунарівськ 947 | Яворів 948 | Яготин 949 | Ялта 950 | Ямпіль 951 | Яремча 952 | Ясинувата 953 | --------------------------------------------------------------------------------