globalmap.py 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-"
  3. # vim: set expandtab tabstop=4 shiftwidth=4:
  4. """
  5. This file is part of the XSSer project, https://xsser.03c8.net
  6. Copyright (c) 2010/2019 | psy <epsylon@riseup.net>
  7. xsser is free software; you can redistribute it and/or modify it under
  8. the terms of the GNU General Public License as published by the Free
  9. Software Foundation version 3 of the License.
  10. xsser is distributed in the hope that it will be useful, but WITHOUT ANY
  11. WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
  12. FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
  13. details.
  14. You should have received a copy of the GNU General Public License along
  15. with xsser; if not, write to the Free Software Foundation, Inc., 51
  16. Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
  17. """
  18. import os
  19. import gtk
  20. import user
  21. import gobject
  22. from core.reporter import XSSerReporter
  23. from core.curlcontrol import Curl
  24. from glib import markup_escape_text
  25. from collections import defaultdict
  26. from threading import Thread
  27. import traceback
  28. import urllib
  29. import urlparse
  30. import math
  31. import cairo
  32. import gzip
  33. import pangocairo
  34. import time
  35. class PointType(object):
  36. checked = 15
  37. success = 10
  38. failed = 5
  39. crawled = 0
  40. crashsite = -1
  41. crash_color = [0.1,0.1,0.1]
  42. checked_color = [0,0.8,0.8]
  43. failed_color = [0.8,0.0,0.0]
  44. success_color = [0.0,0.0,0.8]
  45. crawl_color = [0.0,0.0,0.0]
  46. def gtkcol(col):
  47. return [int(col[0]*65535),int(col[1]*65535),int(col[2]*65535)]
  48. class MapPoint(object):
  49. def __init__(self, lat, lng, ptype, size, text): # 0, 5, 10, 15, 20 -> 20==checked
  50. self.latitude = lat
  51. self.longitude = lng
  52. self.size = size
  53. self.text = text
  54. self.reports = defaultdict(list)
  55. self.reports[ptype].append(text)
  56. self.type = ptype
  57. if ptype == PointType.crawled:
  58. self.color = crawl_color
  59. elif ptype == PointType.failed:
  60. self.color = failed_color
  61. elif ptype == PointType.success:
  62. self.color = success_color
  63. elif ptype == PointType.checked:
  64. self.color = checked_color
  65. else:
  66. self.color = crawl_color
  67. self.gtkcolor = gtkcol(self.color)
  68. def add_reports(self, report_type, reports):
  69. for report_type in set(reports.keys() + self.reports.keys()):
  70. self.reports[report_type].extend(reports[report_type])
  71. class CrashSite(MapPoint):
  72. def __init__(self, lat, lng, size, desturl):
  73. MapPoint.__init__(self, lat, lng, PointType.crashsite, size, desturl)
  74. class DownloadThread(Thread):
  75. def __init__(self, geomap, parent):
  76. Thread.__init__(self)
  77. self.daemon = True
  78. self._map = geomap
  79. self._parent = parent
  80. def run(self):
  81. geo_db_path = self._map.get_geodb_path()
  82. def reportfunc(current, blocksize, filesize):
  83. percent = min(float(current)/(filesize/float(blocksize)),1.0)
  84. self._parent.report_state('downloading map', percent)
  85. if not os.path.exists(os.path.dirname(geo_db_path)):
  86. os.makedirs(os.path.dirname(geo_db_path))
  87. self._parent.report_state('getting city database', 0.0)
  88. try:
  89. urllib.urlretrieve('http://xsser.03c8.net/map/GeoLiteCity.dat.gz',
  90. geo_db_path+'.gz', reportfunc)
  91. except:
  92. try:
  93. urllib.urlretrieve('http://xsser.sf.net/map/GeoLiteCity.dat.gz',
  94. geo_db_path+'.gz', reportfunc)
  95. except:
  96. try:
  97. urllib.urlretrieve('http://geolite.maxmind.com/download/geoip/database/GeoLiteCity.dat.gz',
  98. geo_db_path+'.gz', reportfunc)
  99. except:
  100. self._parent.report_state('error downloading map', 0.0)
  101. self._map.geomap_failed()
  102. else:
  103. self._parent.report_state('map downloaded (restart XSSer!!!!)', 0.0)
  104. f_in = gzip.open(geo_db_path+'.gz', 'rb')
  105. f_out = open(geo_db_path, 'wb')
  106. f_out.write(f_in.read())
  107. f_in.close()
  108. print('deleting gzipped file')
  109. os.remove(geo_db_path+'.gz')
  110. self._map.geomap_ready()
  111. class GlobalMap(gtk.DrawingArea, XSSerReporter):
  112. def __init__(self, parent, pixbuf, onattack=False):
  113. gtk.DrawingArea.__init__(self)
  114. geo_db_path = self.get_geodb_path()
  115. self._parent = parent
  116. self._pixbuf = pixbuf
  117. self._cache_geo = {}
  118. self.geo = None
  119. self._onattack = onattack
  120. if not os.path.exists(geo_db_path):
  121. self._t = DownloadThread(self, parent)
  122. self._t.start()
  123. else:
  124. self.finish_init()
  125. def geomap_ready(self):
  126. gtk.gdk.threads_enter()
  127. gobject.timeout_add(0, self.finish_init)
  128. gtk.gdk.threads_leave()
  129. def geomap_failed(self):
  130. gtk.gdk.threads_enter()
  131. gobject.timeout_add(0, self.failed_init)
  132. gtk.gdk.threads_leave()
  133. def failed_init(self):
  134. if hasattr(self, '_t'):
  135. self._t.join()
  136. delattr(self, '_t')
  137. def finish_init(self):
  138. import GeoIP
  139. if hasattr(self, '_t'):
  140. self._t.join()
  141. delattr(self, '_t')
  142. parent = self._parent
  143. geo_db_path = self.get_geodb_path()
  144. Geo = GeoIP.open(geo_db_path, GeoIP.GEOIP_STANDARD)
  145. self.geo = Geo
  146. self.set_has_tooltip(True)
  147. self._max_points = 200
  148. self._lasttime = 0.0
  149. self.context = None
  150. self.mapcontext = None
  151. self._mappixbuf = None
  152. self._selected = []
  153. self._current_text = ["", 0.0]
  154. self._stats = [0,0,0,0,0,0,0]
  155. self.width = self._pixbuf.get_width()
  156. self.height = self._pixbuf.get_height()
  157. self._min_x = 0
  158. self._max_x = self.width
  159. self._drawn_points = []
  160. self._lines = []
  161. self._frozenlines = []
  162. self._points = []
  163. self._crosses = []
  164. self.connect("expose_event", self.expose)
  165. self.connect("query-tooltip", self.on_query_tooltip)
  166. if self.window:
  167. self.window.invalidate_rect(self.allocation, True)
  168. if not self._onattack:
  169. self.add_test_points()
  170. def get_geodb_path(self):
  171. ownpath = os.path.dirname(os.path.dirname(__file__))
  172. gtkpath = os.path.join(ownpath, 'gtk')
  173. if os.path.exists(os.path.join(gtkpath, 'GeoLiteCity.dat')):
  174. return os.path.join(gtkpath, 'GeoLiteCity.dat')
  175. else:
  176. return os.path.join(user.home, '.xsser', 'GeoLiteCity.dat')
  177. def find_points(self, x, y, distance=9.0):
  178. points = []
  179. self._selected = []
  180. for idx, point in enumerate(self._drawn_points):
  181. d_x = x-point[0]
  182. d_y = y-point[1]
  183. if d_y*d_y+d_x*d_x < distance:
  184. self._points[point[2]].size = 4.0
  185. points.append(self._points[point[2]])
  186. self._selected.append(point[2])
  187. if points:
  188. rect = gtk.gdk.Rectangle(0,0,self.width, self.height)
  189. self.window.invalidate_rect(rect, True)
  190. return points
  191. def on_query_tooltip(self, widget, x, y, keyboard_mode, tooltip):
  192. if not self.geo:
  193. return False
  194. points = self.find_points(x, y)
  195. if points:
  196. text = ""
  197. success = []
  198. finalsuccess = []
  199. failures = []
  200. crawls = []
  201. for point in points:
  202. finalsuccess.extend(point.reports[PointType.checked])
  203. success.extend(point.reports[PointType.success])
  204. failures.extend(point.reports[PointType.failed])
  205. crawls.extend(point.reports[PointType.crawled])
  206. if finalsuccess:
  207. text += "<b>browser checked sucesses:</b>\n"
  208. text += "\n".join(map(lambda s: markup_escape_text(s), finalsuccess))
  209. if failures or success:
  210. text += "\n"
  211. if success:
  212. text += "<b>sucesses:</b>\n"
  213. text += "\n".join(map(lambda s: markup_escape_text(s), success))
  214. if failures:
  215. text += "\n"
  216. if failures:
  217. text += "<b>failures:</b>\n"
  218. text += "\n".join(map(lambda s: markup_escape_text(s), failures))
  219. if crawls and not failures and not success:
  220. text += "<b>crawls:</b>\n"
  221. text += "\n".join(map(lambda s: markup_escape_text(s), crawls))
  222. tooltip.set_markup(str(text))
  223. return True
  224. return False
  225. def add_test_points(self):
  226. self.add_point(0.0, 0.0)
  227. self.add_point(0.0, 5.0)
  228. self.add_point(0.0, 10.0)
  229. self.add_point(0.0, 15.0)
  230. self.add_point(5.0, 0.0)
  231. self.add_point(10.0, 0.0)
  232. self.add_point(15.0, 0.0)
  233. def clear(self):
  234. self._points = []
  235. self._lines = []
  236. self.mapcontext = None
  237. self._frozenlines = []
  238. self._crosses = []
  239. self._stats = [0,0,0,0,0,0,0]
  240. def expose(self, widget, event):
  241. if not self.mapcontext:
  242. self._mappixbuf = self._pixbuf.copy()
  243. self.mapsurface = cairo.ImageSurface.create_for_data(self._mappixbuf.get_pixels_array(),
  244. cairo.FORMAT_ARGB32,
  245. self.width,
  246. self.height,
  247. self._pixbuf.get_rowstride())
  248. self.mapcontext = cairo.Context(self.mapsurface)
  249. self.draw_frozen_lines()
  250. self.context = self.window.cairo_create()
  251. self.context.set_source_surface(self.mapsurface)
  252. self.context.rectangle(event.area.x, event.area.y,
  253. event.area.width, event.area.height)
  254. self.context.clip()
  255. self.context.rectangle(event.area.x, event.area.y,
  256. event.area.width, event.area.height)
  257. self.context.fill()
  258. self.context.set_source_color(gtk.gdk.Color(0,0,0))
  259. self._min_x = 5 # we have the scale at the left for now
  260. self._max_x = 0
  261. if self.geo:
  262. self.draw(self.context)
  263. return False
  264. def add_point(self, lng, lat, point_type=PointType.crawled, desturl="testpoint"):
  265. map_point = MapPoint(lat, lng, point_type, 5.0, desturl)
  266. map_point.x, map_point.y = self.plot_point(lat, lng)
  267. self._points.append(map_point)
  268. def add_cross(self, lng, lat, col=[0,0,0], desturl="testpoint"):
  269. for a in self._crosses:
  270. if a.latitude == lat and a.longitude == lng:
  271. return
  272. crash_site = CrashSite(lat, lng, 5.0, desturl)
  273. crash_site.x, crash_site.y = self.plot_point(lat, lng)
  274. self.adjust_bounds(crash_site.x, crash_site.y)
  275. self._crosses.append(crash_site)
  276. self.queue_redraw()
  277. def insert_point(self, lng, lat, col=[0,0,0], desturl="testpoint"):
  278. self._points.insert(0, MapPoint(lat, lng, point_type, 5.0, desturl))
  279. def _preprocess_points(self):
  280. newpoints = defaultdict(list)
  281. for point in self._points:
  282. key = (point.latitude, point.longitude)
  283. newpoints[key].append(point)
  284. self._points = []
  285. for points in newpoints.itervalues():
  286. win_type = points[0]
  287. win_size = points[0]
  288. for point in points[1:]:
  289. if point.type > win_type.type:
  290. win_type = point
  291. if point.size > win_type.size:
  292. win_size = point
  293. self._points.append(win_type)
  294. if win_type != win_size:
  295. self._points.append(win_size)
  296. for point in points:
  297. if not point in [win_size, win_type]:
  298. win_type.add_reports(point.type, point.reports)
  299. if len(self._points) > self._max_points:
  300. self._points = self._points[:self._max_points]
  301. def draw_frozen_lines(self):
  302. for line in self._lines[len(self._frozenlines):]:
  303. if line[4] <= 0.5:
  304. self.draw_line(self.mapcontext, line)
  305. self._frozenlines.append(line)
  306. def draw(self, context, failures=True):
  307. self._preprocess_points()
  308. if self._lasttime == 0:
  309. self._lasttime = time.time()-0.04
  310. currtime = time.time()
  311. timepassed = currtime - self._lasttime
  312. redraw = False
  313. if failures:
  314. self._drawn_points = []
  315. for cross in reversed(self._crosses):
  316. if cross.size > 0.1:
  317. cross.size -= timepassed*2
  318. else:
  319. self._crosses.remove(cross)
  320. if cross.size > 0.1:
  321. redraw = True
  322. self.draw_cross(cross)
  323. for line in reversed(self._lines[len(self._frozenlines):]):
  324. if line[4] > 0.5:
  325. line[4] -= timepassed*2
  326. if line[4] > 0.5:
  327. redraw = True
  328. self.draw_line(self.context, line)
  329. for idx, point in enumerate(self._points):
  330. if point.type >= PointType.success:
  331. if failures:
  332. continue
  333. else:
  334. if not failures:
  335. continue
  336. if point.size > 1.0 and not idx in self._selected:
  337. point.size -= timepassed*2
  338. redraw = True
  339. elif point.size < 1.0:
  340. point.size = 1.0
  341. self.draw_point(point)
  342. x = point.x
  343. y = point.y
  344. self.adjust_bounds(x, y)
  345. self._drawn_points.append([x, y, idx])
  346. stat_f = 1.0
  347. if failures:
  348. mp = self._max_points
  349. self.draw_bar((-45,-160,crawl_color,(self._stats[0]%mp)*stat_f))
  350. self.draw_bar((-45,-155,failed_color,(self._stats[1]%mp)*stat_f))
  351. self.draw_bar((-45,-150,success_color,(self._stats[2]%mp)*stat_f))
  352. self.draw_bar((-45,-145,checked_color,(self._stats[3]%mp)*stat_f))
  353. if int(self._stats[0] / mp):
  354. self.draw_bar((-46,-160,crawl_color,-2-(self._stats[0]/mp)*stat_f))
  355. if int(self._stats[1] / mp):
  356. self.draw_bar((-46,-155,failed_color,-2-(self._stats[1]/mp)*stat_f))
  357. if int(self._stats[2] / mp):
  358. self.draw_bar((-46,-150,success_color,-2-(self._stats[2]/mp)*stat_f))
  359. if int(self._stats[3] / mp):
  360. self.draw_bar((-46,-145,checked_color,-2-(self._stats[3]/mp)*stat_f))
  361. self.draw(context, False)
  362. else:
  363. if self._current_text[1] > 0.0:
  364. self.draw_text(100, self.height-50, self._current_text[0])
  365. self._current_text[1] -= timepassed*4
  366. self._lasttime = currtime
  367. if redraw:
  368. self.queue_redraw()
  369. def adjust_bounds(self, x, y):
  370. if x-20 < self._min_x:
  371. self._min_x = x-20
  372. elif x+20 > self._max_x:
  373. self._max_x = x+20
  374. def draw_text(self, x, y, text):
  375. self.context.save()
  376. self.context.move_to(x, y)
  377. v = (5.0-self._current_text[1])/5.0
  378. self.context.scale(0.1+max(v, 1.0), 0.1+max(v, 1.0))
  379. self.context.set_source_color(gtk.gdk.Color(*gtkcol((v,)*3)))
  380. u = urlparse.urlparse(text)
  381. self.context.show_text(u.netloc)
  382. self.context.restore()
  383. def draw_bar(self, point):
  384. if point[3]:
  385. self.context.save()
  386. x, y = self.plot_point(point[0], point[1])
  387. self.context.set_source_rgb(*point[2])
  388. self.context.rectangle(x, y, 5, -(2.0+point[3]))
  389. self.context.fill()
  390. self.context.restore()
  391. return x, y
  392. def draw_line(self, context, line):
  393. if line[4]:
  394. context.save()
  395. x, y = self.plot_point(line[0], line[1])
  396. x2, y2 = self.plot_point(line[2], line[3])
  397. self.adjust_bounds(x, y)
  398. self.adjust_bounds(x2, y2)
  399. context.set_line_width(1.0)
  400. context.set_source_rgba(0.0, 0.0, 0.0, float(line[4])/5.0)
  401. context.move_to(x, y)
  402. context.rel_line_to(x2-x, y2-y)
  403. context.stroke()
  404. context.restore()
  405. def draw_point(self, point):
  406. if point.size:
  407. self.context.save()
  408. self.context.set_source_color(gtk.gdk.Color(*point.gtkcolor))
  409. self.context.translate(point.x, point.y)
  410. self.context.arc(0.0, 0.0, 2.4*point.size, 0, 2*math.pi)
  411. self.context.close_path()
  412. self.context.fill()
  413. self.context.restore()
  414. def draw_cross(self, point):
  415. if point.size:
  416. self.context.save()
  417. self.context.translate(point.x, point.y)
  418. self.context.rotate(point.size)
  419. self.context.set_line_width(0.8*point.size)
  420. self.context.set_source_color(gtk.gdk.Color(*point.gtkcolor))
  421. self.context.move_to(-3*point.size, -3*point.size)
  422. self.context.rel_line_to(6*point.size, 6*point.size)
  423. self.context.stroke()
  424. self.context.move_to(-3*point.size, +3*point.size)
  425. self.context.rel_line_to(6*point.size, -6*point.size)
  426. self.context.stroke()
  427. self.context.restore()
  428. def get_latlon_fromurl(self, url):
  429. parsed_url = urlparse.urlparse(url)
  430. split_netloc = parsed_url.netloc.split(":")
  431. if len(split_netloc) == 2:
  432. server_name, port = split_netloc
  433. else:
  434. server_name = parsed_url.netloc
  435. port = None
  436. if server_name in self._cache_geo:
  437. return self._cache_geo[server_name]
  438. Geodata = self.geo.record_by_name(server_name)
  439. if Geodata:
  440. country_name = Geodata['country_name']
  441. longitude = Geodata['longitude']
  442. latitude = Geodata['latitude']
  443. self._cache_geo[server_name] = (latitude, longitude)
  444. return latitude, longitude
  445. def start_attack(self):
  446. self.clear()
  447. def queue_redraw(self):
  448. rect = gtk.gdk.region_rectangle((self._min_x,0,self._max_x-self._min_x,
  449. self.height))
  450. if self.window:
  451. self.window.invalidate_region(rect, True)
  452. del rect
  453. def mosquito_crashed(self, dest_url, reason):
  454. self._current_text = [dest_url, 5.0]
  455. self._stats[4] += 1
  456. try:
  457. lat, lon = self.get_latlon_fromurl(dest_url)
  458. except:
  459. return
  460. self.add_cross(lon, lat, crash_color, dest_url)
  461. gtk.gdk.threads_enter()
  462. self.queue_redraw()
  463. gtk.gdk.threads_leave()
  464. def add_checked(self, dest_url):
  465. self._current_text = [dest_url, 5.0]
  466. self._stats[3] += 1
  467. try:
  468. lat, lon = self.get_latlon_fromurl(dest_url)
  469. except:
  470. return
  471. self.add_point(lon, lat, PointType.checked, dest_url)
  472. gtk.gdk.threads_enter()
  473. self.queue_redraw()
  474. gtk.gdk.threads_leave()
  475. def add_success(self, dest_url):
  476. self._current_text = [dest_url, 5.0]
  477. self._stats[2] += 1
  478. try:
  479. lat, lon = self.get_latlon_fromurl(dest_url)
  480. except:
  481. return
  482. self.add_point(lon, lat, PointType.success, dest_url)
  483. gtk.gdk.threads_enter()
  484. self.queue_redraw()
  485. gtk.gdk.threads_leave()
  486. def add_failure(self, dest_url):
  487. self._current_text = [dest_url, 5.0]
  488. self._stats[1] += 1
  489. try:
  490. lat, lon = self.get_latlon_fromurl(dest_url)
  491. except:
  492. return
  493. self.add_point(lon, lat, PointType.failed, dest_url)
  494. gtk.gdk.threads_enter()
  495. self.queue_redraw()
  496. gtk.gdk.threads_leave()
  497. def add_link(self, orig_url, dest_url):
  498. try:
  499. lat, lon = self.get_latlon_fromurl(orig_url)
  500. except:
  501. return
  502. try:
  503. d_lat, d_lon = self.get_latlon_fromurl(dest_url)
  504. except:
  505. return
  506. if lat == d_lat and lon == d_lon:
  507. return
  508. for a in self._lines:
  509. if a[0] == lat and a[1] == lon and a[2] == d_lat and a[3] == d_lon:
  510. return
  511. self._lines.append([lat, lon, d_lat, d_lon, 0.5])
  512. def start_crawl(self, dest_url):
  513. self._current_text = [dest_url, 5.0]
  514. self._stats[0] += 1
  515. try:
  516. lat, lon = self.get_latlon_fromurl(dest_url)
  517. except:
  518. return
  519. self.add_point(lon, lat, PointType.crawled, dest_url)
  520. gtk.gdk.threads_enter()
  521. self.queue_redraw()
  522. gtk.gdk.threads_leave()
  523. def plot_point_mercator(self, lat, lng):
  524. longitude_shift = -23
  525. map_width = self.width
  526. map_height = self.height
  527. y_pos = -1
  528. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  529. lat = lat * math.pi / 180; # convert from degrees to radians
  530. y = math.log(math.tan((lat/2.0) + (math.pi/4.0)))
  531. y = (map_height / 2.0) - (map_width * y / (2.0*math.pi)) + y_pos
  532. return x, y
  533. def plot_point_mercatormiller(self, lat, lng):
  534. longitude_shift = 0
  535. map_width = self.width
  536. map_height = self.height
  537. y_pos = 70
  538. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  539. lat = lat * math.pi / 180.0; # convert from degrees to radians
  540. y = 1.25*math.log(math.tan((lat/2.5) + (math.pi/4.0)))
  541. y = (map_height / 2.0) - (map_width * y / (2.0*math.pi)) + y_pos
  542. return x, y
  543. def plot_point_equirectangular(self, lat, lng):
  544. longitude_shift = -23
  545. map_width = self.width
  546. map_height = self.height
  547. y_pos = 0
  548. magic_factor = 1.1
  549. x = int((map_width * (180.0 + lng) / 360.0) + longitude_shift) % map_width
  550. y = int((map_height / 2.0) - int((map_height * (lat) / 180.0)*magic_factor))
  551. return x,y
  552. def plot_point(self, lat, lng):
  553. x, y = self.plot_point_equirectangular(lat, lng)
  554. if x-20 < self._min_x:
  555. self._min_x = x-20
  556. if x+20 > self._max_x:
  557. self._max_x = x+20
  558. return x, y