globalmap.py 22 KB

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