From 492c21db495a866841014bd69a62df26e727b424 Mon Sep 17 00:00:00 2001 From: Jxck-S Date: Mon, 16 May 2022 16:35:24 -0400 Subject: [PATCH] Meta, Fuel,TextCred,Overrides --- .gitignore | 3 + Pipfile | 10 +- Pipfile.lock | 624 ++++++++++++++++++++-- __main__.py | 14 +- aircraft_type_fuel_consumption_rates.json | 214 ++++++++ calculate_headings.py | 2 +- configs/mainconf.ini | 11 +- configs/plane1.ini | 25 +- defAirport.py | 76 ++- defDiscord.py | 10 +- defSS.py | 83 ++- defTelegram.py | 38 ++ defTweet.py | 9 - fuel_calc.py | 52 ++ meta_toolkit.py | 51 ++ modify_image.py | 18 +- planeClass.py | 327 ++++++++++-- 17 files changed, 1358 insertions(+), 209 deletions(-) create mode 100644 aircraft_type_fuel_consumption_rates.json create mode 100644 defTelegram.py delete mode 100644 defTweet.py create mode 100644 fuel_calc.py create mode 100644 meta_toolkit.py diff --git a/.gitignore b/.gitignore index 061461e..456c050 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ __pycache__ dependencies testing lookup_route.py +icao_url_gen.py +install.sh +coul_icao_gen.py diff --git a/Pipfile b/Pipfile index 032f728..fd92156 100644 --- a/Pipfile +++ b/Pipfile @@ -18,6 +18,14 @@ selenium = "*" opensky-api = {editable = true, git = "https://github.com/openskynetwork/opensky-api.git", subdirectory = "python"} webdriver-manager = "*" shapely = "*" - +pycairo = "*" +geog = "*" +py-staticmaps = "*" +pyproj = "*" +pandas = "*" +lxml = "*" +beautifulsoup4 = "*" +python-telegram-bot = "*" +configparser = "*" [requires] python_version = "3.9" diff --git a/Pipfile.lock b/Pipfile.lock index 609c146..1d5bd99 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5131229ab384051accd51e665cc63da8e2d08a651ad0c9df09041fca9f306977" + "sha256": "35921a1158bc5b11d88e9f2a4702ee3ae09228a7d5438d1562ba62f24b02fd05" }, "pipfile-spec": 6, "requires": { @@ -16,6 +16,52 @@ ] }, "default": { + "appdirs": { + "hashes": [ + "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", + "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128" + ], + "version": "==1.4.4" + }, + "apscheduler": { + "hashes": [ + "sha256:3bb5229eed6fbbdafc13ce962712ae66e175aa214c69bed35a06bffcf0c5e244", + "sha256:e8b1ecdb4c7cb2818913f766d5898183c7cb8936680710a4d3a966e02262e526" + ], + "version": "==3.6.3" + }, + "async-generator": { + "hashes": [ + "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b", + "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144" + ], + "markers": "python_version >= '3.5'", + "version": "==1.10" + }, + "attrs": { + "hashes": [ + "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4", + "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", + "version": "==21.4.0" + }, + "beautifulsoup4": { + "hashes": [ + "sha256:9a315ce70049920ea4572a4055bc4bd700c940521d36fc858205ad4fcde149bf", + "sha256:c23ad23c521d818955a4151a67d81580319d4bf548d3d49f4223ae041ff98891" + ], + "index": "pypi", + "version": "==4.10.0" + }, + "cachetools": { + "hashes": [ + "sha256:2cc0b89715337ab6dbba85b5b50effe2b0c74e035d83ee8ed637cf52f12ae001", + "sha256:61b5ed1e22a0924aed1d23b478f37e8d52549ff8a961de2909c69bf950020cff" + ], + "markers": "python_version ~= '3.5'", + "version": "==4.2.2" + }, "certifi": { "hashes": [ "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872", @@ -23,6 +69,62 @@ ], "version": "==2021.10.8" }, + "cffi": { + "hashes": [ + "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3", + "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2", + "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636", + "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20", + "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728", + "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27", + "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66", + "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443", + "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0", + "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7", + "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39", + "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605", + "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a", + "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37", + "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029", + "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139", + "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc", + "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df", + "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14", + "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880", + "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2", + "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a", + "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e", + "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474", + "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024", + "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8", + "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0", + "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e", + "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a", + "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e", + "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032", + "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6", + "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e", + "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b", + "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e", + "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954", + "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962", + "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c", + "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4", + "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55", + "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962", + "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023", + "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c", + "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6", + "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8", + "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382", + "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7", + "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc", + "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997", + "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796" + ], + "markers": "os_name == 'nt' and implementation_name != 'pypy'", + "version": "==1.15.0" + }, "charset-normalizer": { "hashes": [ "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597", @@ -44,23 +146,55 @@ "sha256:1b35798fdf1713f1c3139016cfcbc461f09edbf099d1fb658d4b7479fcaa3daa", "sha256:e8b39238fb6f0153a069aa253d349467c3c4737934f253ef6abac5fe0eca1e5d" ], - "markers": "python_version >= '3.6'", + "index": "pypi", "version": "==5.2.0" }, - "crayons": { + "cryptography": { "hashes": [ - "sha256:bd33b7547800f2cfbd26b38431f9e64b487a7de74a947b0fafc89b45a601813f", - "sha256:e73ad105c78935d71fe454dd4b85c5c437ba199294e7ffd3341842bc683654b1" + "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b", + "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51", + "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7", + "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d", + "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6", + "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29", + "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9", + "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf", + "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815", + "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf", + "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85", + "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77", + "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86", + "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb", + "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e", + "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0", + "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3", + "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84", + "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2", + "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6" ], - "version": "==0.4.0" + "version": "==36.0.2" }, "discord-webhook": { "hashes": [ - "sha256:17e475d8a52fe0bfa26b071925f55087600e9bb96e821b611dc463f4b4998c89", - "sha256:f3d660df572caaa9c2621edd7e8634a70d6d8295ce9256c365838312457069a1" + "sha256:1557a3a86ec556d5fceeff3d0c8601affd83770280b7358a3c7f4fb7c8ee32e5", + "sha256:5c59e3c5b52be8d9273aebdc5647f564a3c24ce4f75635fd1d9e4d9d8dad30f4" ], "index": "pypi", - "version": "==0.14.0" + "version": "==0.15.0" + }, + "future": { + "hashes": [ + "sha256:b1bead90b70cf6ec3f0710ae53a525360fa360d306a86583adc6bf83a4db537d" + ], + "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==0.18.2" + }, + "geog": { + "hashes": [ + "sha256:9b6b020b72bf135d49299115e5a4a751f2432def4d4cd87d10b48a5ae51ec643" + ], + "index": "pypi", + "version": "==0.0.2" }, "geographiclib": { "hashes": [ @@ -77,6 +211,14 @@ "index": "pypi", "version": "==2.2.0" }, + "h11": { + "hashes": [ + "sha256:70813c1135087a248a4d38cc0e1a0181ffab2188141a93eaf567940c3957ff06", + "sha256:8ddd78563b633ca55346c8cd41ec0af27d3c79931828beffb46ce70a379e7442" + ], + "markers": "python_version >= '3.6'", + "version": "==0.13.0" + }, "idna": { "hashes": [ "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff", @@ -85,6 +227,99 @@ "markers": "python_version >= '3'", "version": "==3.3" }, + "lxml": { + "hashes": [ + "sha256:078306d19a33920004addeb5f4630781aaeabb6a8d01398045fcde085091a169", + "sha256:0c1978ff1fd81ed9dcbba4f91cf09faf1f8082c9d72eb122e92294716c605428", + "sha256:1010042bfcac2b2dc6098260a2ed022968dbdfaf285fc65a3acf8e4eb1ffd1bc", + "sha256:1d650812b52d98679ed6c6b3b55cbb8fe5a5460a0aef29aeb08dc0b44577df85", + "sha256:20b8a746a026017acf07da39fdb10aa80ad9877046c9182442bf80c84a1c4696", + "sha256:2403a6d6fb61c285969b71f4a3527873fe93fd0abe0832d858a17fe68c8fa507", + "sha256:24f5c5ae618395ed871b3d8ebfcbb36e3f1091fd847bf54c4de623f9107942f3", + "sha256:28d1af847786f68bec57961f31221125c29d6f52d9187c01cd34dc14e2b29430", + "sha256:31499847fc5f73ee17dbe1b8e24c6dafc4e8d5b48803d17d22988976b0171f03", + "sha256:31ba2cbc64516dcdd6c24418daa7abff989ddf3ba6d3ea6f6ce6f2ed6e754ec9", + "sha256:330bff92c26d4aee79c5bc4d9967858bdbe73fdbdbacb5daf623a03a914fe05b", + "sha256:5045ee1ccd45a89c4daec1160217d363fcd23811e26734688007c26f28c9e9e7", + "sha256:52cbf2ff155b19dc4d4100f7442f6a697938bf4493f8d3b0c51d45568d5666b5", + "sha256:530f278849031b0eb12f46cca0e5db01cfe5177ab13bd6878c6e739319bae654", + "sha256:545bd39c9481f2e3f2727c78c169425efbfb3fbba6e7db4f46a80ebb249819ca", + "sha256:5804e04feb4e61babf3911c2a974a5b86f66ee227cc5006230b00ac6d285b3a9", + "sha256:5a58d0b12f5053e270510bf12f753a76aaf3d74c453c00942ed7d2c804ca845c", + "sha256:5f148b0c6133fb928503cfcdfdba395010f997aa44bcf6474fcdd0c5398d9b63", + "sha256:5f7d7d9afc7b293147e2d506a4596641d60181a35279ef3aa5778d0d9d9123fe", + "sha256:60d2f60bd5a2a979df28ab309352cdcf8181bda0cca4529769a945f09aba06f9", + "sha256:6259b511b0f2527e6d55ad87acc1c07b3cbffc3d5e050d7e7bcfa151b8202df9", + "sha256:6268e27873a3d191849204d00d03f65c0e343b3bcb518a6eaae05677c95621d1", + "sha256:627e79894770783c129cc5e89b947e52aa26e8e0557c7e205368a809da4b7939", + "sha256:62f93eac69ec0f4be98d1b96f4d6b964855b8255c345c17ff12c20b93f247b68", + "sha256:6d6483b1229470e1d8835e52e0ff3c6973b9b97b24cd1c116dca90b57a2cc613", + "sha256:6f7b82934c08e28a2d537d870293236b1000d94d0b4583825ab9649aef7ddf63", + "sha256:6fe4ef4402df0250b75ba876c3795510d782def5c1e63890bde02d622570d39e", + "sha256:719544565c2937c21a6f76d520e6e52b726d132815adb3447ccffbe9f44203c4", + "sha256:730766072fd5dcb219dd2b95c4c49752a54f00157f322bc6d71f7d2a31fecd79", + "sha256:74eb65ec61e3c7c019d7169387d1b6ffcfea1b9ec5894d116a9a903636e4a0b1", + "sha256:7993232bd4044392c47779a3c7e8889fea6883be46281d45a81451acfd704d7e", + "sha256:80bbaddf2baab7e6de4bc47405e34948e694a9efe0861c61cdc23aa774fcb141", + "sha256:86545e351e879d0b72b620db6a3b96346921fa87b3d366d6c074e5a9a0b8dadb", + "sha256:891dc8f522d7059ff0024cd3ae79fd224752676447f9c678f2a5c14b84d9a939", + "sha256:8a31f24e2a0b6317f33aafbb2f0895c0bce772980ae60c2c640d82caac49628a", + "sha256:8b99ec73073b37f9ebe8caf399001848fced9c08064effdbfc4da2b5a8d07b93", + "sha256:986b7a96228c9b4942ec420eff37556c5777bfba6758edcb95421e4a614b57f9", + "sha256:a1547ff4b8a833511eeaceacbcd17b043214fcdb385148f9c1bc5556ca9623e2", + "sha256:a2bfc7e2a0601b475477c954bf167dee6d0f55cb167e3f3e7cefad906e7759f6", + "sha256:a3c5f1a719aa11866ffc530d54ad965063a8cbbecae6515acbd5f0fae8f48eaa", + "sha256:a9f1c3489736ff8e1c7652e9dc39f80cff820f23624f23d9eab6e122ac99b150", + "sha256:aa0cf4922da7a3c905d000b35065df6184c0dc1d866dd3b86fd961905bbad2ea", + "sha256:ad4332a532e2d5acb231a2e5d33f943750091ee435daffca3fec0a53224e7e33", + "sha256:b2582b238e1658c4061ebe1b4df53c435190d22457642377fd0cb30685cdfb76", + "sha256:b6fc2e2fb6f532cf48b5fed57567ef286addcef38c28874458a41b7837a57807", + "sha256:b92d40121dcbd74831b690a75533da703750f7041b4bf951befc657c37e5695a", + "sha256:bbab6faf6568484707acc052f4dfc3802bdb0cafe079383fbaa23f1cdae9ecd4", + "sha256:c0b88ed1ae66777a798dc54f627e32d3b81c8009967c63993c450ee4cbcbec15", + "sha256:ce13d6291a5f47c1c8dbd375baa78551053bc6b5e5c0e9bb8e39c0a8359fd52f", + "sha256:db3535733f59e5605a88a706824dfcb9bd06725e709ecb017e165fc1d6e7d429", + "sha256:dd10383f1d6b7edf247d0960a3db274c07e96cf3a3fc7c41c8448f93eac3fb1c", + "sha256:e01f9531ba5420838c801c21c1b0f45dbc9607cb22ea2cf132844453bec863a5", + "sha256:e11527dc23d5ef44d76fef11213215c34f36af1608074561fcc561d983aeb870", + "sha256:e1ab2fac607842ac36864e358c42feb0960ae62c34aa4caaf12ada0a1fb5d99b", + "sha256:e1fd7d2fe11f1cb63d3336d147c852f6d07de0d0020d704c6031b46a30b02ca8", + "sha256:e9f84ed9f4d50b74fbc77298ee5c870f67cb7e91dcdc1a6915cb1ff6a317476c", + "sha256:ec4b4e75fc68da9dc0ed73dcdb431c25c57775383fec325d23a770a64e7ebc87", + "sha256:f10ce66fcdeb3543df51d423ede7e238be98412232fca5daec3e54bcd16b8da0", + "sha256:f63f62fc60e6228a4ca9abae28228f35e1bd3ce675013d1dfb828688d50c6e23", + "sha256:fa56bb08b3dd8eac3a8c5b7d075c94e74f755fd9d8a04543ae8d37b1612dd170", + "sha256:fa9b7c450be85bfc6cd39f6df8c5b8cbd76b5d6fc1f69efec80203f9894b885f" + ], + "index": "pypi", + "version": "==4.8.0" + }, + "numpy": { + "hashes": [ + "sha256:07a8c89a04997625236c5ecb7afe35a02af3896c8aa01890a849913a2309c676", + "sha256:08d9b008d0156c70dc392bb3ab3abb6e7a711383c3247b410b39962263576cd4", + "sha256:201b4d0552831f7250a08d3b38de0d989d6f6e4658b709a02a73c524ccc6ffce", + "sha256:2c10a93606e0b4b95c9b04b77dc349b398fdfbda382d2a39ba5a822f669a0123", + "sha256:3ca688e1b9b95d80250bca34b11a05e389b1420d00e87a0d12dc45f131f704a1", + "sha256:48a3aecd3b997bf452a2dedb11f4e79bc5bfd21a1d4cc760e703c31d57c84b3e", + "sha256:568dfd16224abddafb1cbcce2ff14f522abe037268514dd7e42c6776a1c3f8e5", + "sha256:5bfb1bb598e8229c2d5d48db1860bcf4311337864ea3efdbe1171fb0c5da515d", + "sha256:639b54cdf6aa4f82fe37ebf70401bbb74b8508fddcf4797f9fe59615b8c5813a", + "sha256:8251ed96f38b47b4295b1ae51631de7ffa8260b5b087808ef09a39a9d66c97ab", + "sha256:92bfa69cfbdf7dfc3040978ad09a48091143cffb778ec3b03fa170c494118d75", + "sha256:97098b95aa4e418529099c26558eeb8486e66bd1e53a6b606d684d0c3616b168", + "sha256:a3bae1a2ed00e90b3ba5f7bd0a7c7999b55d609e0c54ceb2b076a25e345fa9f4", + "sha256:c34ea7e9d13a70bf2ab64a2532fe149a9aced424cd05a2c4ba662fd989e3e45f", + "sha256:dbc7601a3b7472d559dc7b933b18b4b66f9aa7452c120e87dfb33d02008c8a18", + "sha256:e7927a589df200c5e23c57970bafbd0cd322459aa7b1ff73b7c2e84d6e3eae62", + "sha256:f8c1f39caad2c896bc0018f699882b345b2a63708008be29b1f355ebf6f933fe", + "sha256:f950f8845b480cffe522913d35567e29dd381b0dc7e4ce6a4a9f9156417d2430", + "sha256:fade0d4f4d292b6f39951b6836d7a3c7ef5b2347f3c420cd9820a1d90d794802", + "sha256:fdf3c08bce27132395d3c3ba1503cac12e17282358cb4bddc25cc46b0aca07aa" + ], + "markers": "python_version < '3.10' and platform_machine != 'aarch64' and platform_machine != 'arm64'", + "version": "==1.22.3" + }, "oauthlib": { "hashes": [ "sha256:23a8208d75b902797ea29fd31fa80a15ed9dc2c6c16fe73f5d346f83f6fa27a2", @@ -96,9 +331,44 @@ "opensky-api": { "editable": true, "git": "https://github.com/openskynetwork/opensky-api.git", - "ref": "d576cf260affd99156e352528ea46817273512d7", + "ref": "68c8304471fe32f5eaca8a41144fdad35d8a8ce4", "subdirectory": "python" }, + "outcome": { + "hashes": [ + "sha256:c7dd9375cfd3c12db9801d080a3b63d4b0a261aa996c4c13152380587288d958", + "sha256:e862f01d4e626e63e8f92c38d1f8d5546d3f9cce989263c521b2e7990d186967" + ], + "markers": "python_version >= '3.6'", + "version": "==1.1.0" + }, + "pandas": { + "hashes": [ + "sha256:0259cd11e7e6125aaea3af823b80444f3adad6149ff4c97fef760093598b3e34", + "sha256:04dd15d9db538470900c851498e532ef28d4e56bfe72c9523acb32042de43dfb", + "sha256:0b1a13f647e4209ed7dbb5da3497891d0045da9785327530ab696417ef478f84", + "sha256:19f7c632436b1b4f84615c3b127bbd7bc603db95e3d4332ed259dc815c9aaa26", + "sha256:1b384516dbb4e6aae30e3464c2e77c563da5980440fbdfbd0968e3942f8f9d70", + "sha256:1d85d5f6be66dfd6d1d8d13b9535e342a2214260f1852654b19fa4d7b8d1218b", + "sha256:2e5a7a1e0ecaac652326af627a3eca84886da9e667d68286866d4e33f6547caf", + "sha256:3129a35d9dad1d80c234dd78f8f03141b914395d23f97cf92a366dcd19f8f8bf", + "sha256:358b0bc98a5ff067132d23bf7a2242ee95db9ea5b7bbc401cf79205f11502fd3", + "sha256:3dfb32ed50122fe8c5e7f2b8d97387edd742cc78f9ec36f007ee126cd3720907", + "sha256:4e1176f45981c8ccc8161bc036916c004ca51037a7ed73f2d2a9857e6dbe654f", + "sha256:508c99debccd15790d526ce6b1624b97a5e1e4ca5b871319fb0ebfd46b8f4dad", + "sha256:6105af6533f8b63a43ea9f08a2ede04e8f43e49daef0209ab0d30352bcf08bee", + "sha256:6d6ad1da00c7cc7d8dd1559a6ba59ba3973be6b15722d49738b2be0977eb8a0c", + "sha256:7ea47ba1d6f359680130bd29af497333be6110de8f4c35b9211eec5a5a9630fa", + "sha256:8db93ec98ac7cb5f8ac1420c10f5e3c43533153f253fe7fb6d891cf5aa2b80d2", + "sha256:96e9ece5759f9b47ae43794b6359bbc54805d76e573b161ae770c1ea59393106", + "sha256:bbb15ad79050e8b8d39ec40dd96a30cd09b886a2ae8848d0df1abba4d5502a67", + "sha256:c614001129b2a5add5e3677c3a213a9e6fd376204cb8d17c04e84ff7dfc02a73", + "sha256:e6a7bbbb7950063bfc942f8794bc3e31697c020a14f1cd8905fc1d28ec674a01", + "sha256:f02e85e6d832be37d7f16cf6ac8bb26b519ace3e5f3235564a91c7f658ab2a43" + ], + "index": "pypi", + "version": "==1.4.1" + }, "pillow": { "hashes": [ "sha256:011233e0c42a4a7836498e98c1acf5e744c96a67dd5032a6f666cc1fb97eab97", @@ -148,6 +418,84 @@ "index": "pypi", "version": "==0.12.0" }, + "py-staticmaps": { + "hashes": [ + "sha256:5aba5ad59f30a63f860e76ed99407a6efb24eaad5c8997aa8617363989f17389" + ], + "index": "pypi", + "version": "==0.4.0" + }, + "pycairo": { + "hashes": [ + "sha256:251907f18a552df938aa3386657ff4b5a4937dde70e11aa042bc297957f4b74b", + "sha256:26b72b813c6f9d495f71057eab89c13e70a21c92360e9265abc049e0a931fa39", + "sha256:31e1c4850db03201d33929cbe1905ce1b33202683ebda7bb0d4dba489115066b", + "sha256:4357f20a6b1de8f1e8072a74ff68ab4c9a0ae698cd9f5c0f2b2cdd9b28b635f6", + "sha256:44a2ecf34968de07b3b9dfdcdbccbd25aa3cab267200f234f84e81481a73bbf6", + "sha256:6d37375aab9f2bb6136f076c19815d72108383baae89fbc0d6cb8e5092217d02", + "sha256:70936b19f967fa3cb3cd200c2608911227fa5d09dae21c166f64bc15e714ee41", + "sha256:dace6b356c476de27f8e1522428ac21a799c225703f746e2957d441f885dcb6c", + "sha256:f63c153a9ea3d21aff85e2caeee4b0c5d566b2368b4ed64826020d12953d76a4" + ], + "index": "pypi", + "version": "==1.21.0" + }, + "pycparser": { + "hashes": [ + "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9", + "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206" + ], + "version": "==2.21" + }, + "pyopenssl": { + "hashes": [ + "sha256:660b1b1425aac4a1bea1d94168a85d99f0b3144c869dd4390d27629d0087f1bf", + "sha256:ea252b38c87425b64116f808355e8da644ef9b07e429398bfece610f893ee2e0" + ], + "version": "==22.0.0" + }, + "pyproj": { + "hashes": [ + "sha256:067a5c6099949edd66e9a10b139af4e2f65ebadb9f59583923a1d3feefac749a", + "sha256:0e1fd560b509b722db6566fa9685252f25640e93464d09e13d5190ed7ab491ba", + "sha256:235b52d8700ffb6be1f3638b1e25d83a9c13edcdb793236d8a98fd39227c5c27", + "sha256:277072176a17471c0b1d25d6cae75401d81e9b50ea625ba546f5b79acd757dfc", + "sha256:2c41c9b7b5e1a1b0acc2b7b2f5de65b226f7b96c870888e4f679ff96322b1ed0", + "sha256:3d28b84913cd849832a8f154c0e0c2ee4618057f7389ee68bfdb2145e7ed78cc", + "sha256:4125e6704751d0e82d8d912d9851da097e8d38599d4c45f9944faaeb21771938", + "sha256:44b5590c0b8dd002154916e170ef88f57abf91005b34bcb23faef97abb4d42c2", + "sha256:4d2fc49c73d9f34e932bf37926d56916ba1b6f2f693cd4d8cc1d0d9eacc0e537", + "sha256:5a105bfe37c78416d2641cd5d3368c99057d041f15f8d51ea3898953b21395c9", + "sha256:99f171da5f885efeec8d7fb2e2557175ffa8834eeb488842b1f52ac78a9a98e5", + "sha256:ab4baf781721640659db83a6b4da636fc403008f4978c668275754284c946778", + "sha256:b15e199c1da8fd132e11dfa68d8cf65d4812dedabc776b308df778ecd0d07658", + "sha256:b48dd9e5736957707fce1d9253fb0772bcf80480198c7790e21fed73fee61240", + "sha256:ce1adec823738e2d7c6af019fc38f58b4204bacfc782e4430373578c672f3833", + "sha256:ce8bfbc212729e9a643f5f5d77f7a93394e032eda1e2d8799ae902d08add747e", + "sha256:dbf479bd481774ad217e9db5674868eee8f01dfe3868f61753328895ae7da61a", + "sha256:e70a1ea6f198cace1a492397bdd0a46e640201120973293d6c48031e370d6a87", + "sha256:eca8ecf2b6b3225d93c723e6a2f51143d9195ac407f69e979363cdde344b93bb", + "sha256:fcceb6736085bf19291b707bc67c8cebe05330bd02268e9b8eba6d28a1905fce" + ], + "index": "pypi", + "version": "==3.3.0" + }, + "pysocks": { + "hashes": [ + "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299", + "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5", + "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0" + ], + "version": "==1.7.1" + }, + "python-dateutil": { + "hashes": [ + "sha256:0123cacc1627ae19ddf3c27a5de5bd67ee4586fbdd6440d9748f8abb483d3e86", + "sha256:961d03dc3453ebbc59dbdea9e4e11c5651520a876d0f4db161e8674aae935da9" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==2.8.2" + }, "python-magic": { "hashes": [ "sha256:1a2c81e8f395c744536369790bd75094665e9644110a6623bcc3bbea30f03973", @@ -156,13 +504,37 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", "version": "==0.4.25" }, + "python-slugify": { + "hashes": [ + "sha256:00003397f4e31414e922ce567b3a4da28cf1436a53d332c9aeeb51c7d8c469fd", + "sha256:8c0016b2d74503eb64761821612d58fcfc729493634b1eb0575d8f5b4aa1fbcf" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==6.1.1" + }, + "python-telegram-bot": { + "hashes": [ + "sha256:534f5bb0ff4ca34c9252e97e0b3bcdab81d97be0eb4821682a361cb426c00e55", + "sha256:baeff704baa2ac3dc17a944c02da888228ad258e89be2e5bcbd13a8a5102d573" + ], + "index": "pypi", + "version": "==13.11" + }, "pytz": { "hashes": [ - "sha256:83a4a90894bf38e243cf052c8b58f381bfe9a7a483f6a9cab140bc7f702ac4da", - "sha256:eb10ce3e7736052ed3623d49975ce333bcd712c7bb19a58b9e2089d4057d0798" + "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7", + "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c" ], "index": "pypi", - "version": "==2021.1" + "version": "==2022.1" + }, + "pytz-deprecation-shim": { + "hashes": [ + "sha256:8314c9692a636c8eb3bda879b9f119e350e93223ae83e70e80c31675a0fdc1a6", + "sha256:af097bae1b616dde5c5744441e2ddc69e74dfdcb0c263129610d85b87445a59d" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4, 3.5'", + "version": "==0.1.0.post0" }, "requests": { "hashes": [ @@ -180,42 +552,90 @@ "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", "version": "==1.3.1" }, + "s2sphere": { + "hashes": [ + "sha256:c2478c1ff7c601a59a7151a57b605435897514578fa6bdb8730721c182adbbaf", + "sha256:d2340c9cf458ddc9a89afd1d8048a4195ce6fa6b0095ab900d4be5271e537401" + ], + "version": "==0.2.5" + }, "selenium": { "hashes": [ - "sha256:2d7131d7bc5a5b99a2d9b04aaf2612c411b03b8ca1b1ee8d3de5845a9be2cb3c", - "sha256:deaf32b60ad91a4611b98d8002757f29e6f2c2d5fcaf202e1c9ad06d6772300d" + "sha256:14d28a628c831c105d38305c881c9c7847199bfd728ec84240c5e86fa1c9bd5a" ], "index": "pypi", - "version": "==3.141.0" + "version": "==4.1.3" }, "shapely": { "hashes": [ - "sha256:052eb5b9ba756808a7825e8a8020fb146ec489dd5c919e7d139014775411e688", - "sha256:1641724c1055459a7e2b8bbe47ba25bdc89554582e62aec23cb3f3ca25f9b129", - "sha256:17df66e87d0fe0193910aeaa938c99f0b04f67b430edb8adae01e7be557b141b", - "sha256:182716ffb500d114b5d1b75d7fd9d14b7d3414cef3c38c0490534cc9ce20981a", - "sha256:2df5260d0f2983309776cb41bfa85c464ec07018d88c0ecfca23d40bfadae2f1", - "sha256:35be1c5d869966569d3dfd4ec31832d7c780e9df760e1fe52131105685941891", - "sha256:46da0ea527da9cf9503e66c18bab6981c5556859e518fe71578b47126e54ca93", - "sha256:4c10f317e379cc404f8fc510cd9982d5d3e7ba13a9cfd39aa251d894c6366798", - "sha256:4f3c59f6dbf86a9fc293546de492f5e07344e045f9333f3a753f2dda903c45d1", - "sha256:60e5b2282619249dbe8dc5266d781cc7d7fb1b27fa49f8241f2167672ad26719", - "sha256:617bf046a6861d7c6b44d2d9cb9e2311548638e684c2cd071d8945f24a926263", - "sha256:6593026cd3f5daaea12bcc51ae5c979318070fefee210e7990cb8ac2364e79a1", - "sha256:6871acba8fbe744efa4f9f34e726d070bfbf9bffb356a8f6d64557846324232b", - "sha256:791477edb422692e7dc351c5ed6530eb0e949a31b45569946619a0d9cd5f53cb", - "sha256:8e7659dd994792a0aad8fb80439f59055a21163e236faf2f9823beb63a380e19", - "sha256:8f15b6ce67dcc05b61f19c689b60f3fe58550ba994290ff8332f711f5aaa9840", - "sha256:90a3e2ae0d6d7d50ff2370ba168fbd416a53e7d8448410758c5d6a5920646c1d", - "sha256:a3774516c8a83abfd1ddffb8b6ec1b0935d7fe6ea0ff5c31a18bfdae567b4eba", - "sha256:a5c3a50d823c192f32615a2a6920e8c046b09e07a58eba220407335a9cd2e8ea", - "sha256:b40cc7bb089ae4aa9ddba1db900b4cd1bce3925d2a4b5837b639e49de054784f", - "sha256:da38ed3d65b8091447dc3717e5218cc336d20303b77b0634b261bc5c1aa2bae8", - "sha256:de618e67b64a51a0768d26a9963ecd7d338a2cf6e9e7582d2385f88ad005b3d1", - "sha256:e3afccf0437edc108eef1e2bb9cc4c7073e7705924eb4cd0bf7715cd1ef0ce1b" + "sha256:0ca96a3314b7a38a3bb385531469de1fcf2b2c2979ec2aa4f37b4c70632cf1ad", + "sha256:2020fda37c708d44a613c020cea09e81e476f96866f348afc2601e66c0e71db1", + "sha256:2381ce0aff67d569eb509bcc051264aa5fbdc1fdd54f4c09963d0e09f16a8f1b", + "sha256:363df36370f28fdc7789857929f6ff27e659f64087b4c89f7a47ed43bd3bfe4d", + "sha256:3a3602ba2e7715ddd5d4114173dec83d3181bfb2497e8589676c284aa739fd67", + "sha256:3e792635e92c9aacd1452a589a4fa2970114b6a9b1165e09655481f6e58970f5", + "sha256:437fff3b6274be26ffa3e450de711ee01e436324b5a405952add2146227e3eb5", + "sha256:44cb895b1710f7559c28d69dfa08cafe4f58cd4b7a87091a55bdf6711ad9ad66", + "sha256:493902923fdd135316161a4ece5294ba3ce81accaa54540d2af3b93f7231143a", + "sha256:54aeb2a57978ce731fd52289d0e1deee7c232d41aed53091f38776378f644184", + "sha256:5a420e7112b55a1587412a5b03ebf59e302ddd354da68516d3721718f6b8a7c5", + "sha256:679789d774cfe09ca05118cab78c0a6a42985b3ed23bc93606272a4509b4df28", + "sha256:69d5352fb977655c85d2f40a05ae24fc5053cccee77d0a8b1f773e54804e723e", + "sha256:83f3c8191d30ae0e3dd557434c48ca591d75342d5a3f42fc5148ec42796be624", + "sha256:89bc5f3abc1ccbc7682c2e1664153c4f8f125fa9c24bff4abca48685739d5636", + "sha256:8cf7331f61780506976fe2175e069d898e1b04ace73be21aad55c3ee92e58e3a", + "sha256:9248aad099ecf228fbdd877b0c668823dd83c48798cf04d49a1be75167e3a7ce", + "sha256:93ff06ff05fbe2be843b93c7b1ad8292e56e665ba01b4708f75ae8a757972e9f", + "sha256:aea1e87450adffba3d04ccbaa790df719bb7aa23b05ac797ad16be236a5d0db8", + "sha256:b4d35e72022b2dbf152d476b0362596011c674ff68be9fc8f2e68e71d86502ca", + "sha256:b82fc74d5efb11a71283c4ed69b4e036997cc70db4b73c646207ddf0476ade44", + "sha256:bab5ff7c576588acccd665ecce2a0fe7b47d4ce0398f2d5c1e5b2e27d09398d2", + "sha256:bc6063875182515d3888180cc4cbdbaa6443e4a4386c4bb25499e9875b75dcac", + "sha256:c4c366e18edf91196a399f8f0f046f93516002e6d8af0b57c23e7c7d91944b16", + "sha256:dc0f46212f84c57d13189fc33cf61e13eee292704d7652e931e4b51c54b0c73c", + "sha256:f109064bdb0753a6bac6238538cfeeb4a09739e2d556036b343b2eabeb9520b2" ], "index": "pypi", - "version": "==1.7.1" + "version": "==1.8.1.post1" + }, + "six": { + "hashes": [ + "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926", + "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254" + ], + "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", + "version": "==1.16.0" + }, + "sniffio": { + "hashes": [ + "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663", + "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de" + ], + "markers": "python_version >= '3.5'", + "version": "==1.2.0" + }, + "sortedcontainers": { + "hashes": [ + "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", + "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0" + ], + "version": "==2.4.0" + }, + "soupsieve": { + "hashes": [ + "sha256:1a3cca2617c6b38c0343ed661b1fa5de5637f257d4fe22bd9f1338010a1efefb", + "sha256:b8d49b1cd4f037c7082a9683dfa1801aa2597fb11c3a1155b7a5b94829b4f1f9" + ], + "markers": "python_version >= '3.6'", + "version": "==2.3.1" + }, + "svgwrite": { + "hashes": [ + "sha256:ca63d76396d1f6f099a2b2d8cf1419e1c1de8deece9a2b7f4da0632067d71d43", + "sha256:d304a929f197d31647c287c10eee0f64378058e1c83a9df83433a5980864e59f" + ], + "markers": "python_version >= '3.6'", + "version": "==1.4.2" }, "tabulate": { "hashes": [ @@ -225,37 +645,135 @@ "index": "pypi", "version": "==0.8.9" }, + "text-unidecode": { + "hashes": [ + "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8", + "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93" + ], + "version": "==1.3" + }, + "tornado": { + "hashes": [ + "sha256:0a00ff4561e2929a2c37ce706cb8233b7907e0cdc22eab98888aca5dd3775feb", + "sha256:0d321a39c36e5f2c4ff12b4ed58d41390460f798422c4504e09eb5678e09998c", + "sha256:1e8225a1070cd8eec59a996c43229fe8f95689cb16e552d130b9793cb570a288", + "sha256:20241b3cb4f425e971cb0a8e4ffc9b0a861530ae3c52f2b0434e6c1b57e9fd95", + "sha256:25ad220258349a12ae87ede08a7b04aca51237721f63b1808d39bdb4b2164558", + "sha256:33892118b165401f291070100d6d09359ca74addda679b60390b09f8ef325ffe", + "sha256:33c6e81d7bd55b468d2e793517c909b139960b6c790a60b7991b9b6b76fb9791", + "sha256:3447475585bae2e77ecb832fc0300c3695516a47d46cefa0528181a34c5b9d3d", + "sha256:34ca2dac9e4d7afb0bed4677512e36a52f09caa6fded70b4e3e1c89dbd92c326", + "sha256:3e63498f680547ed24d2c71e6497f24bca791aca2fe116dbc2bd0ac7f191691b", + "sha256:548430be2740e327b3fe0201abe471f314741efcb0067ec4f2d7dcfb4825f3e4", + "sha256:6196a5c39286cc37c024cd78834fb9345e464525d8991c21e908cc046d1cc02c", + "sha256:61b32d06ae8a036a6607805e6720ef00a3c98207038444ba7fd3d169cd998910", + "sha256:6286efab1ed6e74b7028327365cf7346b1d777d63ab30e21a0f4d5b275fc17d5", + "sha256:65d98939f1a2e74b58839f8c4dab3b6b3c1ce84972ae712be02845e65391ac7c", + "sha256:66324e4e1beede9ac79e60f88de548da58b1f8ab4b2f1354d8375774f997e6c0", + "sha256:6c77c9937962577a6a76917845d06af6ab9197702a42e1346d8ae2e76b5e3675", + "sha256:70dec29e8ac485dbf57481baee40781c63e381bebea080991893cd297742b8fd", + "sha256:7250a3fa399f08ec9cb3f7b1b987955d17e044f1ade821b32e5f435130250d7f", + "sha256:748290bf9112b581c525e6e6d3820621ff020ed95af6f17fedef416b27ed564c", + "sha256:7da13da6f985aab7f6f28debab00c67ff9cbacd588e8477034c0652ac141feea", + "sha256:8f959b26f2634a091bb42241c3ed8d3cedb506e7c27b8dd5c7b9f745318ddbb6", + "sha256:9de9e5188a782be6b1ce866e8a51bc76a0fbaa0e16613823fc38e4fc2556ad05", + "sha256:a48900ecea1cbb71b8c71c620dee15b62f85f7c14189bdeee54966fbd9a0c5bd", + "sha256:b87936fd2c317b6ee08a5741ea06b9d11a6074ef4cc42e031bc6403f82a32575", + "sha256:c77da1263aa361938476f04c4b6c8916001b90b2c2fdd92d8d535e1af48fba5a", + "sha256:cb5ec8eead331e3bb4ce8066cf06d2dfef1bfb1b2a73082dfe8a161301b76e37", + "sha256:cc0ee35043162abbf717b7df924597ade8e5395e7b66d18270116f8745ceb795", + "sha256:d14d30e7f46a0476efb0deb5b61343b1526f73ebb5ed84f23dc794bdb88f9d9f", + "sha256:d371e811d6b156d82aa5f9a4e08b58debf97c302a35714f6f45e35139c332e32", + "sha256:d3d20ea5782ba63ed13bc2b8c291a053c8d807a8fa927d941bd718468f7b950c", + "sha256:d3f7594930c423fd9f5d1a76bee85a2c36fd8b4b16921cae7e965f22575e9c01", + "sha256:dcef026f608f678c118779cd6591c8af6e9b4155c44e0d1bc0c87c036fb8c8c4", + "sha256:e0791ac58d91ac58f694d8d2957884df8e4e2f6687cdf367ef7eb7497f79eaa2", + "sha256:e385b637ac3acaae8022e7e47dfa7b83d3620e432e3ecb9a3f7f58f150e50921", + "sha256:e519d64089b0876c7b467274468709dadf11e41d65f63bba207e04217f47c085", + "sha256:e7229e60ac41a1202444497ddde70a48d33909e484f96eb0da9baf8dc68541df", + "sha256:ed3ad863b1b40cd1d4bd21e7498329ccaece75db5a5bf58cd3c9f130843e7102", + "sha256:f0ba29bafd8e7e22920567ce0d232c26d4d47c8b5cf4ed7b562b5db39fa199c5", + "sha256:fa2ba70284fa42c2a5ecb35e322e68823288a4251f9ba9cc77be04ae15eada68", + "sha256:fba85b6cd9c39be262fcd23865652920832b61583de2a2ca907dbd8e8a8c81e5" + ], + "markers": "python_version >= '3.5'", + "version": "==6.1" + }, + "trio": { + "hashes": [ + "sha256:670a52d3115d0e879e1ac838a4eb999af32f858163e3a704fe4839de2a676070", + "sha256:fb2d48e4eab0dfb786a472cd514aaadc71e3445b203bc300bad93daa75d77c1a" + ], + "markers": "python_version >= '3.7'", + "version": "==0.20.0" + }, + "trio-websocket": { + "hashes": [ + "sha256:5b558f6e83cc20a37c3b61202476c5295d1addf57bd65543364e0337e37ed2bc", + "sha256:a3d34de8fac26023eee701ed1e7bf4da9a8326b61a62934ec9e53b64970fd8fe" + ], + "markers": "python_version >= '3.5'", + "version": "==0.9.2" + }, "tweepy": { "hashes": [ - "sha256:b28d72073b794141ad1cfdc34cb52b83b14d6b34cd4c5ea9d47bb85159b656e8", - "sha256:bd3af045bed9cd6a838c32a1344d48191096e5cb930012452f8397b12c89d785" + "sha256:8ba5774ac1663b09e5fce1b030daf076f2c9b3ddbf2e7e7ea0bae762e3b1fe3e", + "sha256:f281bb53ab3ba999ff5e3d743d92d3ed543ee5551c7250948f9e56190ec7a43e" ], "index": "pypi", - "version": "==4.0.0" + "version": "==4.8.0" + }, + "tzdata": { + "hashes": [ + "sha256:238e70234214138ed7b4e8a0fab0e5e13872edab3be586ab8198c407620e2ab9", + "sha256:8b536a8ec63dc0751342b3984193a3118f8fca2afe25752bb9b7fffd398552d3" + ], + "markers": "python_version >= '3.6'", + "version": "==2022.1" + }, + "tzlocal": { + "hashes": [ + "sha256:0f28015ac68a5c067210400a9197fc5d36ba9bc3f8eaf1da3cbd59acdfed9e09", + "sha256:28ba8d9fcb6c9a782d6e0078b4f6627af1ea26aeaa32b4eab5324abc7df4149f" + ], + "markers": "python_version >= '3.6'", + "version": "==4.1" }, "urllib3": { + "extras": [ + "secure", + "socks" + ], "hashes": [ - "sha256:000ca7f471a233c2251c6c7023ee85305721bfdf18621ebff4fd17a8653427ed", - "sha256:0e7c33d9a63e7ddfcb86780aac87befc2fbddf46c58dbb487e0855f7ceec283c" + "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14", + "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e" ], "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'", - "version": "==1.26.8" + "version": "==1.26.9" }, "webdriver-manager": { "hashes": [ - "sha256:50a6e174106542f5335cacc387cec7ada26812babc1aeca61c208a1bab2ac2c5", - "sha256:c6d81590aae6fc0fb10cf7dd20c8c1b9bb043501f9cf62c316a854a0de841e32" + "sha256:2eb7c2fe38ec5b06e2090164923e4dfb7c3ac4e7140333a3de9c7956f5047858", + "sha256:b5b91b5df83181e002263fe27296967a5b19cb1ebe8e4a63ee83538394037df4" ], "index": "pypi", - "version": "==3.4.2" + "version": "==3.5.4" }, "websocket-client": { "hashes": [ - "sha256:074e2ed575e7c822fc0940d31c3ac9bb2b1142c303eafcf3e304e6ce035522e8", - "sha256:6278a75065395418283f887de7c3beafb3aa68dada5cacbe4b214e8d26da499b" + "sha256:50b21db0058f7a953d67cc0445be4b948d7fc196ecbeb8083d68d94628e4abf6", + "sha256:722b171be00f2b90e1d4fb2f2b53146a536ca38db1da8ff49c972a4e1365d0ef" ], - "markers": "python_version >= '3.6'", - "version": "==1.3.1" + "markers": "python_version >= '3.7'", + "version": "==1.3.2" + }, + "wsproto": { + "hashes": [ + "sha256:2218cb57952d90b9fca325c0dcfb08c3bda93e8fd8070b0a17f048e2e47a521b", + "sha256:a2e56bfd5c7cd83c1369d83b5feccd6d37798b74872866e62616e0ecf111bda8" + ], + "markers": "python_version >= '3.7'", + "version": "==1.1.0" } }, "develop": {} diff --git a/__main__.py b/__main__.py index 232480a..4ac9890 100644 --- a/__main__.py +++ b/__main__.py @@ -1,4 +1,5 @@ import configparser +from logging import DEBUG import time from colorama import Fore, Back, Style import platform @@ -48,11 +49,11 @@ main_config.read('./configs/mainconf.ini') source = main_config.get('DATA', 'SOURCE') if main_config.getboolean('DISCORD', 'ENABLE'): from defDiscord import sendDis - sendDis("Started", main_config) + sendDis("Started", main_config, role_id = main_config.get('DISCORD', 'ROLE_ID')) def service_exit(signum, frame): if main_config.getboolean('DISCORD', 'ENABLE'): from defDiscord import sendDis - sendDis("Service Stop", main_config) + sendDis("Service Stop", main_config, role_id = main_config.get('DISCORD', 'ROLE_ID')) raise SystemExit("Service Stop") signal.signal(signal.SIGTERM, service_exit) if os.path.isfile("lookup_route.py"): @@ -82,6 +83,7 @@ try: except pytz.exceptions.UnknownTimeZoneError: tz = pytz.UTC last_ra_count = None + print(len(planes), "Planes configured") while True: datetime_tz = datetime.now(tz) if datetime_tz.hour == 0 and datetime_tz.minute == 0: @@ -138,12 +140,12 @@ try: for planeData in data['ac']: data_indexed[planeData[icao_key].upper()] = planeData for key, obj in planes.items(): - try: + if key in data_indexed.keys(): if api_version == 1: obj.run_adsbx_v1(data_indexed[key.upper()]) elif api_version == 2: obj.run_adsbx_v2(data_indexed[key.upper()]) - except KeyError: + else: obj.run_empty() else: for obj in planes.values(): @@ -206,10 +208,10 @@ except Exception as e: except OSError: pass import logging - logging.basicConfig(filename='crash_latest.log', filemode='w', format='%(asctime)s - %(message)s') + logging.basicConfig(filename='crash_latest.log', filemode='w', format='%(asctime)s - %(message)s',level=logging.DEBUG) logging.Formatter.converter = time.gmtime logging.error(e) logging.error(str(traceback.format_exc())) from defDiscord import sendDis - sendDis(str("Error Exiting: " + str(e) + "Failed on " + key), main_config, "crash_latest.log") + sendDis(str("Error Exiting: " + str(e) + " Failed on " + "https://globe.adsbexchange.com/?icao=" + key), main_config, main_config.get('DISCORD', 'ROLE_ID'), "crash_latest.log") raise e \ No newline at end of file diff --git a/aircraft_type_fuel_consumption_rates.json b/aircraft_type_fuel_consumption_rates.json new file mode 100644 index 0000000..742a3e9 --- /dev/null +++ b/aircraft_type_fuel_consumption_rates.json @@ -0,0 +1,214 @@ +{ + "EA50": { + "name": "Eclipse 550", + "galph": 76, + "category": "VLJ" + }, + "LJ31": { + "name": "Learjet 31", + "galph": 202, + "category": "Light" + }, + "LJ40": { + "name": "Learjet 40", + "galph": 207, + "category": "Light" + }, + "PC24": { + "name": "Pilatus PC-24", + "galph": 154, + "category": "Light" + }, + "LJ45": { + "name": "Learjet 45", + "galph": 205, + "category": "Super Light" + }, + "LJ70": { + "name": "Learjet 70", + "galph": 198, + "category": "Super Light" + }, + "LJ75": { + "name": "Learjet 75", + "galph": 199, + "category": "Super Light" + }, + "G150": { + "name": "Gulfstream G150", + "galph": 228, + "category": "Midsize" + }, + "LJ60": { + "name": "Learjet 60", + "galph": 239, + "category": "Midsize" + }, + "GALX": { + "name": "Gulfstream G200", + "galph": 278, + "category": "Super Midsize" + }, + "G280": { + "name": "Gulfstream G280", + "galph": 297, + "category": "Super Midsize" + }, + "GLF5": { + "name": "Gulfstream G500", + "galph": 447, + "category": "Large" + }, + "GLF6": { + "name": "Gulfstream G650", + "galph": 503, + "category": "Ultra Long Range" + }, + "PC12": { + "name": "Pilatus PC-12", + "galph": 66, + "category": "Turboprop Aircraft" + }, + "GLEX": { + "name": "Global", + "galph": 500, + "category": "Ultra Long Range" + } + , + "CL30": { + "name": "Challenger 300", + "galph": 295, + "category": "Super Midsize" + }, "B742": { + "name": "Boeing 747-200", + "galph": 3830, + "category": "Large" + }, "T38": { + "name": "T-38 Talon", + "galph": 375, + "category": "Fighter" + }, "WB57": { + "name": "Martin B-57 Canberra", + "galph": 531, + "category": "Twinjet Tactical Bomber and Reconnaissance" + }, "B74S": { + "name": "747 SP", + "galph": 2289, + "category": "Large" + }, "B752": { + "name": "757 200", + "galph": 877, + "category": "Large" + }, + "B738": { + "name": "737 800", + "galph": 832, + "category": "Medium" + }, + "B737": { + "name": "737 700", + "galph": 796, + "category": "Medium" + }, + "A320": { + "name": "A320", + "galph": 800, + "category": "Medium" + }, + "P3": { + "name": "Lockheed Orion P3", + "galph": 671, + "category": "Turboprop" + }, + "C750": { + "name": "Cessna 750 Citation X", + "galph": 347, + "category": "Small Private Jet" + }, + "FA7X": { + "name": "Dassult Falcon 7X", + "galph": 380, + "category": "Small Private Jet" + }, + "F900": { + "name": "Dassult Falcon 900", + "galph": 347, + "category": "Small Private Jet" + }, + "H25B": { + "name": "Hawker 750/850", + "galph": 270, + "category": "Small Private Jet" + }, + "C680": { + "name": "Cessna 680 Citation", + "galph": 247, + "category": "Small Private Jet" + }, + "GLF3": { + "name": "Gulfstream 3", + "galph": 568, + "category": "Heavy Private Jet" + }, + "GLF4": { + "name": "Gulfstream 4", + "galph": 479, + "category": "Heavy Private Jet" + }, + "CL60": { + "name": "Bombardier CL-600 Challenge", + "galph": 262, + "category": "Mid-size Private Jet" + }, + "A139": { + "name": "Agusta-Bell AW139", + "galph": 150, + "category": "Medium Utility Helicopter" + }, + "GL5T": { + "name": "Global 5000", + "galph": 455, + "category": "Heavy Private Jet" + }, + "GA6C": { + "name": "Gulfstream G600", + "galph": 458, + "category": "Heavy Private Jet" + }, + "A337": { + "name": "Airbus Beluga XL", + "galph": 1800, + "category": "Large Transport Aircraft" + }, + "A3ST": { + "name": "Airbus Beluga", + "galph": 1260, + "category": "Large Transport Aircraft" + }, + "F2TH": { + "name": "Dassault Falcon 2000", + "galph": 245, + "category": "Medium Private Jet" + }, + "GA5C": { + "name": "Gulfstream G500", + "galph": 402, + "category": "Large Private Jet" + }, + "C130": { + "name": "Lockheed C130", + "galph": 740, + "category": "Medium Cargo" + }, + "B762": { + "name": "Boeing 767 200", + "galph": 1722, + "category": "Wide-body" + }, + "B772": { + "name": "Boeing 777 200", + "galph": 2300, + "category": "Wide-body" + } + +} \ No newline at end of file diff --git a/calculate_headings.py b/calculate_headings.py index 20e5697..2ac8ac0 100644 --- a/calculate_headings.py +++ b/calculate_headings.py @@ -15,7 +15,6 @@ def calculate_cardinal(d): dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW'] ix = int(round(d / (360. / len(dirs)))) card = dirs[ix % len(dirs)] - print(card) return card def calculate_deg_change(new_heading, original_heading): """Calculates change between two headings, returns negative degree if change is left, positive if right""" @@ -29,6 +28,7 @@ def calculate_deg_change(new_heading, original_heading): track_change = normal if direction == "left": track_change *= -1 + track_change = round(track_change, 2) print(f"Track change of {track_change}° which is {direction}") return track_change diff --git a/configs/mainconf.ini b/configs/mainconf.ini index 729ae53..17fb045 100644 --- a/configs/mainconf.ini +++ b/configs/mainconf.ini @@ -36,4 +36,13 @@ API_KEY = googleapikey [DISCORD] ENABLE = FALSE USERNAME = usernamehere -URL = webhookurl \ No newline at end of file +URL = webhookurl + +[TFRS] +URL = http://127.0.0.1:5000/detailed_all + +[TWITTER] +#GLOBAL TWITTER CONSUMER KEY AND CONSUMERS SECRET ONLY NEED TO BE HERE NOW +ENABLE = False +CONSUMER_KEY = ck +CONSUMER_SECRET = cs diff --git a/configs/plane1.ini b/configs/plane1.ini index 9e77914..4a71b1d 100644 --- a/configs/plane1.ini +++ b/configs/plane1.ini @@ -2,8 +2,12 @@ #Plane to track, based of ICAO or ICAO24 which is the unique transponder address of a plane. ICAO = icaohere -#Optional Per Plane Override -#DATA_LOSS_MINS = 20 + +#Optional Per Plane Overrides +; OVERRIDE_REG= +; OVERRIDE_ICAO_TYPE= +; OVERRIDE_TYPELONG = +; DATA_LOSS_MINS = 20 [MAP] #Map to create from Google Static Maps or screenshot global tar1090 from globe.adsbexchange.com @@ -21,8 +25,6 @@ TYPES = [small_airport, medium_airport, large_airport] [TWITTER] ENABLE = FALSE TITLE = -CONSUMER_KEY = ckhere -CONSUMER_SECRET = cshere ACCESS_TOKEN = athere ACCESS_TOKEN_SECRET = atshere @@ -39,4 +41,17 @@ URL = webhookurl #Role to tag optional, the role ID ROLE_ID = Title = -USERNAME = plane-notify \ No newline at end of file +USERNAME = plane-notify + +[META] +ENABLE = False +FB_PAGE_ID = +IG_USER_ID = +ACCESS_TOKEN = + + +[TELEGRAM] +ENABLE = False +TITLE = +ROOM_ID = +BOT_TOKEN = diff --git a/defAirport.py b/defAirport.py index 073ad46..ed64da8 100644 --- a/defAirport.py +++ b/defAirport.py @@ -1,45 +1,41 @@ import csv import math - - def add_airport_region(airport_dict): - # Get full region/state name from iso region name - with open('./dependencies/regions.csv', 'r', encoding='utf-8') as regions_csv: - regions_csv = csv.DictReader(filter(lambda row: row[0] != '#', regions_csv)) - for region in regions_csv: - if region['code'] == airport_dict['iso_region']: - airport_dict['region'] = region['name'] - return airport_dict - - + #Get full region/state name from iso region name + with open('./dependencies/regions.csv', 'r', encoding='utf-8') as regions_csv: + regions_csv = csv.DictReader(filter(lambda row: row[0]!='#', regions_csv)) + for region in regions_csv: + if region['code'] == airport_dict['iso_region']: + airport_dict['region'] = region['name'] + return airport_dict def getClosestAirport(latitude, longitude, allowed_types): - from geopy.distance import geodesic - plane = (latitude, longitude) - closest_airport_dict = dict() - with open('./dependencies/airports.csv', 'r', encoding='utf-8') as airport_csv: - airport_csv_reader = csv.DictReader(filter(lambda row: row[0] != '#', airport_csv)) - for airport in airport_csv_reader: - if airport['type'] in allowed_types: - airport_coord = float(airport['latitude_deg']), float(airport['longitude_deg']) - airport_dist = float((geodesic(plane, airport_coord).mi)) - if not closest_airport_dict or airport_dist < closest_airport_dist: - closest_airport_dict = airport - closest_airport_dist = airport_dist - closest_airport_dict['distance_mi'] = closest_airport_dist - # Convert indent key to icao key as its labeled icao in other places not ident - closest_airport_dict['icao'] = closest_airport_dict.pop('gps_code') - closest_airport_dict = add_airport_region(closest_airport_dict) - return closest_airport_dict - - + from geopy.distance import geodesic + plane = (latitude, longitude) + with open('./dependencies/airports.csv', 'r', encoding='utf-8') as airport_csv: + airport_csv_reader = csv.DictReader(filter(lambda row: row[0]!='#', airport_csv)) + for airport in airport_csv_reader: + if airport['type'] in allowed_types: + airport_coord = float(airport['latitude_deg']), float(airport['longitude_deg']) + airport_dist = float((geodesic(plane, airport_coord).mi)) + if "closest_airport_dict" not in locals(): + closest_airport_dict = airport + closest_airport_dist = airport_dist + elif airport_dist < closest_airport_dist: + closest_airport_dict = airport + closest_airport_dist = airport_dist + closest_airport_dict['distance_mi'] = closest_airport_dist + #Convert indent key to icao key as its labeled icao in other places not ident + closest_airport_dict['icao'] = closest_airport_dict.pop('gps_code') + closest_airport_dict = add_airport_region(closest_airport_dict) + return closest_airport_dict def get_airport_by_icao(icao): - with open('./dependencies/airports.csv', 'r', encoding='utf-8') as airport_csv: - airport_csv_reader = csv.DictReader(filter(lambda row: row[0] != '#', airport_csv)) - for airport in airport_csv_reader: - if airport['gps_code'] == icao: - matching_airport = airport - # Convert indent key to icao key as its labeled icao in other places not ident - matching_airport['icao'] = matching_airport.pop('gps_code') - break - matching_airport = add_airport_region(matching_airport) - return matching_airport + with open('./dependencies/airports.csv', 'r', encoding='utf-8') as airport_csv: + airport_csv_reader = csv.DictReader(filter(lambda row: row[0]!='#', airport_csv)) + for airport in airport_csv_reader: + if airport['gps_code'] == icao: + matching_airport = airport + #Convert indent key to icao key as its labeled icao in other places not ident + matching_airport['icao'] = matching_airport.pop('gps_code') + break + matching_airport = add_airport_region(matching_airport) + return matching_airport \ No newline at end of file diff --git a/defDiscord.py b/defDiscord.py index eb6e71c..b0bc1ab 100644 --- a/defDiscord.py +++ b/defDiscord.py @@ -1,12 +1,14 @@ -def sendDis(message, config, file_name = None, role_id = None): +def sendDis(message, config, role_id = None, *file_names): import requests from discord_webhook import DiscordWebhook if role_id != None: message += f" <@&{role_id}>" webhook = DiscordWebhook(url=config.get('DISCORD', 'URL'), content=message[0:1999], username=config.get('DISCORD', 'USERNAME')) - if file_name != None: - with open(file_name, "rb") as f: - webhook.add_file(file=f.read(), filename=file_name) + + if file_names != []: + for file_name in file_names: + with open(file_name, "rb") as f: + webhook.add_file(file=f.read(), filename=file_name) try: webhook.execute() except requests.exceptions.RequestException: diff --git a/defSS.py b/defSS.py index d29d607..d831e38 100644 --- a/defSS.py +++ b/defSS.py @@ -1,22 +1,28 @@ +import json from selenium import webdriver +from selenium.webdriver.chrome.service import Service from webdriver_manager.chrome import ChromeDriverManager import time from selenium.webdriver.support.ui import WebDriverWait from selenium.webdriver.common.by import By -def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_track_labels=False): + +def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_track_labels=False, overrides={}): chrome_options = webdriver.ChromeOptions() chrome_options.headless = True chrome_options.add_argument('window-size=800,800') chrome_options.add_argument('ignore-certificate-errors') - chrome_options.add_argument("--enable-logging --v=1") + #Plane images issue loading when in headless setting agent fixes. + chrome_options.add_argument("user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/101.0.4951.54 Safari/537.36") import os import platform if platform.system() == "Linux" and os.geteuid()==0: chrome_options.add_argument('--no-sandbox') # required when running as root user. otherwise you would get no sandbox errors. - browser = webdriver.Chrome(ChromeDriverManager().install(), options=chrome_options) + browser = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=chrome_options) url = f"https://globe.adsbexchange.com/?{url_params}" + print(url) browser.set_page_load_timeout(80) browser.get(url) + WebDriverWait(browser, 40).until(lambda d: d.execute_script("return jQuery.active == 0")) remove_id_elements = ["show_trace", "credits", 'infoblock_close', 'selected_photo_link', "history_collapse"] for element in remove_id_elements: try: @@ -35,37 +41,58 @@ def get_adsbx_screenshot(file_path, url_params, enable_labels=False, enable_trac except: print("Couldn't disable sidebar on map") #Remove share - try: - element = browser.find_element_by_xpath("//*[contains(text(), 'Share')]") - browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element) - except: - print("Couldn't remove share button from map") + # try: + # element = browser.find_element_by_xpath("//*[contains(text(), 'Copy Link')]") + # browser.execute_script("""var element = arguments[0]; element.parentNode.removeChild(element); """, element) + # except Exception as e: + # print("Couldn't remove share button from map", e) #browser.execute_script("toggleFollow()") if enable_labels: browser.find_element_by_tag_name('body').send_keys('l') if enable_track_labels: browser.find_element_by_tag_name('body').send_keys('k') - WebDriverWait(browser, 40).until(lambda d: d.execute_script("return jQuery.active == 0")) - try: - photo_box = browser.find_element_by_id("silhouette") - except: - pass - else: - import requests, json - photo_list = json.loads(requests.get("https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/photo-list.json").text) - if "icao" in url_params: - import re + from selenium.webdriver.support import expected_conditions as EC + time.sleep(15) - icao = re.search('icao=(.+?)&', url_params).group(1).lower() - print(icao) - if icao in photo_list.keys(): - browser.execute_script("arguments[0].id = 'airplanePhoto';", photo_box) - browser.execute_script(f"arguments[0].src = 'https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/images/{photo_list[icao]['reg']}.jpg';", photo_box) - copyright = browser.find_element_by_id("copyrightInfo") - browser.execute_script("arguments[0].id = 'copyrightInfoFreeze';", copyright) - browser.execute_script("$('#copyrightInfoFreeze').css('font-size', '12px');") - browser.execute_script(f"arguments[0].appendChild(document.createTextNode('Image © {photo_list[icao]['photographer']}'))", copyright) + if 'reg' in overrides.keys(): + element = browser.find_element_by_id("selected_registration") + browser.execute_script(f"arguments[0].innerText = '* {overrides['reg']}'", element) + reg = overrides['reg'] + else: + try: + reg = browser.find_element_by_id("selected_registration").get_attribute("innerHTML") + print(reg) + except Exception as e: + print("Couldn't find reg in tar1090", e) + reg = None + if reg is not None: + try: + try: + myElem = WebDriverWait(browser, 150).until(EC.presence_of_element_located((By.ID, 'silhouette'))) + photo_box = browser.find_element_by_id("silhouette") + except: + photo_box = browser.find_element_by_id("airplanePhoto") + finally: + import requests, json + photo_list = json.loads(requests.get("https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/photo-list.json").text) + if reg in photo_list.keys(): + browser.execute_script("arguments[0].id = 'airplanePhoto';", photo_box) + browser.execute_script(f"arguments[0].src = 'https://raw.githubusercontent.com/Jxck-S/aircraft-photos/main/images/{reg}.jpg';", photo_box) + copyright = browser.find_element_by_id("copyrightInfo") + browser.execute_script("arguments[0].id = 'copyrightInfoFreeze';", copyright) + browser.execute_script("$('#copyrightInfoFreeze').css('font-size', '12px');") + #browser.execute_script("""var element = arguments[0]; + #element.parentNode.removeChild(element.firstChild);""", copyright) + browser.execute_script(f"arguments[0].appendChild(document.createTextNode('Image © {photo_list[reg]['photographer']}'))", copyright) + except Exception as e: + print("Error on changing photo", e) + if 'type' in overrides.keys(): + element = browser.find_element_by_id("selected_icaotype") + browser.execute_script(f"arguments[0].innerText = '* {overrides['type']}'", element) + if 'typelong' in overrides.keys(): + element = browser.find_element_by_id("selected_typelong") + browser.execute_script(f"arguments[0].innerText = '* {overrides['typelong']}'", element) time.sleep(5) browser.save_screenshot(file_path) browser.quit() @@ -76,4 +103,4 @@ def generate_adsbx_screenshot_time_params(timestamp): print(timestamp_dt) start_time = timestamp_dt - timedelta(minutes=1) time_params = "&showTrace=" + timestamp_dt.strftime("%Y-%m-%d") + "&startTime=" + start_time.strftime("%H:%M:%S") + "&endTime=" + timestamp_dt.strftime("%H:%M:%S") - return time_params \ No newline at end of file + return time_params diff --git a/defTelegram.py b/defTelegram.py new file mode 100644 index 0000000..10d8298 --- /dev/null +++ b/defTelegram.py @@ -0,0 +1,38 @@ +def sendTeleg(photo, message, config): + import telegram + sent = False + retry_c = 0 + while sent == False: + try: + bot = telegram.Bot(token=config.get('TELEGRAM', 'BOT_TOKEN'), request=telegram.utils.request.Request(connect_timeout=20, read_timeout=20)) + sent = bot.send_photo(chat_id=config.get('TELEGRAM', 'ROOM_ID'), photo=photo, caption=message, parse_mode=telegram.ParseMode.MARKDOWN, timeout=20, ) + except Exception as err: + print('err.args:') + print(err.args) + print(f"Unexpected {err=}, {type(err)=}") + print("\nString err:\n"+str(err)) + if retry_c > 4: + print('Telegram attempts exceeded. Message not sent.') + break + elif str(err) == 'Unauthorized': + print('Invalid Telegram bot token, message not sent.') + break + elif str(err) == 'Timed out': + retry_c += 1 + print('Telegram timeout count: '+str(retry_c)) + pass + elif str(err) == 'Chat not found': + print('Invalid Telegram Chat ID, message not sent.') + break + elif str(err)[:35] == '[Errno 2] No such file or directory': + print('Telegram module couldn\'t find an image to send.') + break + elif str(err) == 'Media_caption_too_long': + print('Telegram image caption lenght exceeds 1024 characters. Message not send.') + break + else: + print('[X] Unknown Telegram error. Message not sent.') + break + else: + print("Telegram message successfully sent.") + return sent \ No newline at end of file diff --git a/defTweet.py b/defTweet.py deleted file mode 100644 index a6dd749..0000000 --- a/defTweet.py +++ /dev/null @@ -1,9 +0,0 @@ -# Authenticate to Twitter -def tweepysetup(config): - import tweepy - #DOCU - #https://realpython.com/twitter-bot-python-tweepy/ - auth = tweepy.OAuthHandler(config.get('TWITTER', 'CONSUMER_KEY'), config.get('TWITTER', 'CONSUMER_SECRET')) - auth.set_access_token(config.get('TWITTER', 'ACCESS_TOKEN'), config.get('TWITTER', 'ACCESS_TOKEN_SECRET')) - tweet_api = tweepy.API(auth, wait_on_rate_limit=True) - return tweet_api \ No newline at end of file diff --git a/fuel_calc.py b/fuel_calc.py new file mode 100644 index 0000000..d5c95ba --- /dev/null +++ b/fuel_calc.py @@ -0,0 +1,52 @@ +import json +import requests +def get_avg_fuel_price(): + import pandas as pd + from bs4 import BeautifulSoup + try: + response = requests.get("https://www.airnav.com/fuel/report.html") + soup = BeautifulSoup(response.text, 'lxml') # Parse the HTML as a string + table = soup.find_all('table')[3] # Grab the first table + nation_wide = table.find_all('tr')[4] + nation_wide_avg_jeta = nation_wide.find_all('td')[6] + print("Current nationwide Jet-A fuel price avg per G is $", nation_wide_avg_jeta.text[1:]) + return(float(nation_wide_avg_jeta.text[1:])) + except Exception as e: + print(e) + return None + +def fuel_calculation(aircraft_icao_type, minutes): + """Calculates fuel usage, price, c02 output of a flight depending on aircraft type and flight length""" + with open("aircraft_type_fuel_consumption_rates.json", "r") as f: + fuellist = json.loads(f.read()) + #avg_fuel_price_per_gallon = 5.08 + fuel_flight_info = {} + if aircraft_icao_type in fuellist.keys(): + avg_fuel_price_per_gallon = get_avg_fuel_price() + galph = fuellist[aircraft_icao_type]["galph"] + fuel_used_gal = round(galph * (minutes/60), 2) + fuel_flight_info["fuel_price"] = round(fuel_used_gal * avg_fuel_price_per_gallon) + fuel_used_kg = fuel_used_gal * 3.04 + c02_tons = round((fuel_used_kg * 3.15 ) / 907.185) + fuel_flight_info['fuel_used_kg'] = round(fuel_used_kg) + fuel_flight_info["fuel_used_gal"] = round(fuel_used_gal) + fuel_flight_info['fuel_used_lters'] = round(fuel_used_gal*3.78541) + fuel_flight_info["fuel_used_pds"] = round(fuel_used_kg * 2.20462) + fuel_flight_info["c02_tons"] = c02_tons + print ("Fuel info", fuel_flight_info) + return fuel_flight_info + else: + print("Can't calculate fuel info unknown aircraft ICAO type") + return None + +def fuel_message(fuel_info): + cost = "{:,}".format(fuel_info['fuel_price']) + gallons = "{:,}".format(fuel_info['fuel_used_gal']) + lters = "{:,}".format(fuel_info['fuel_used_lters']) + pds = "{:,}".format(fuel_info['fuel_used_pds']) + kgs = "{:,}".format(fuel_info['fuel_used_kg']) + fuel_message = f"~ {gallons} gallons ({lters} liters). \n~ {pds} pds ({kgs} kg) of jet fuel used. \n~ ${cost} cost of fuel. \n~ {fuel_info['c02_tons']} tons of CO2 emissions." + print(fuel_message) + return fuel_message +#fuel_info = fuel_calculation("B738", 180) +#fuel_message(fuel_info) diff --git a/meta_toolkit.py b/meta_toolkit.py new file mode 100644 index 0000000..c985643 --- /dev/null +++ b/meta_toolkit.py @@ -0,0 +1,51 @@ +import requests +import json +def post_fb(page_id, file_path, message, access_token): + """Posts to Facebook with Image""" + import os + file_name = os.path.basename(file_path) + files= {'image':(file_name, open(file_path, 'rb'), "multipart/form-data")} + url = f"https://graph.facebook.com/{page_id}/photos?message={message}&access_token={access_token}" + resp = requests.post(url, files=files) + resp.raise_for_status() + print("Facebook Post Response: ", resp.json()) + return resp.json() + +def get_fb_post_image_link(post_id, access_token): + """Returns Highest Resolution image link of a Facebook Post by FBID""" + url = f"https://graph.facebook.com/{post_id}?fields=images&access_token={access_token}" + resp = requests.get(url) + resp.raise_for_status() + image_url = resp.json()['images'][0]['source'] + print("Highest Resoulution Image URL for FBID", post_id, "is", image_url) + return image_url + +def post_to_instagram(ig_user_id, access_token, image_url, caption): + """Posts to Instagram""" + post_url = f'https://graph.facebook.com/v13.0/{ig_user_id}/media' + payload = { + 'caption': caption, + 'access_token': access_token, + 'image_url': image_url + } + resp = requests.post(post_url, data=payload) + resp.raise_for_status() + print("IG Media Response:", resp.json()) + result = json.loads(resp.text) + if 'id' in result: + creation_id = result['id'] + second_url = f'https://graph.facebook.com/v13.0/{ig_user_id}/media_publish' + second_payload = { + 'creation_id': creation_id, + 'access_token':access_token + } + resp = requests.post(second_url, data=second_payload) + resp.raise_for_status() + print('Posted to Instagram', caption, "IG response:", resp.json()) + else: + print('Could not post to Instagram: ', resp.json()) +def post_to_meta_both(fb_page_id, ig_user_id, file_path, message, access_token): + """Posts to Facebook and Instagram""" + post_info = post_fb(fb_page_id, file_path, message, access_token) + fb_image_link = get_fb_post_image_link(post_info['id'], access_token) + post_to_instagram(ig_user_id, access_token, fb_image_link, message) diff --git a/modify_image.py b/modify_image.py index df4d59f..9db6e81 100644 --- a/modify_image.py +++ b/modify_image.py @@ -1,4 +1,4 @@ -def append_airport(filename, airport): +def append_airport(filename, airport, text_credit=None): from PIL import Image, ImageDraw, ImageFont distance_mi = airport['distance_mi'] icao = airport['icao'] @@ -27,15 +27,17 @@ def append_airport(filename, airport): #Header Box draw.rectangle(((401, 738), (549, 760)), fill= navish) #ADSBX Logo - draw.rectangle(((658, 762), (800, 782)), fill= white) - adsbx = Image.open("./dependencies/ADSBX_Logo.png") - adsbx = adsbx.resize((25, 25), Image.ANTIALIAS) - image.paste(adsbx, (632, 757), adsbx) + # + # adsbx = Image.open("./dependencies/ADSBX_Logo.png") + # adsbx = adsbx.resize((25, 25), Image.ANTIALIAS) + # image.paste(adsbx, (632, 757), adsbx) #Create Text #ADSBX Credit - (x, y) = (660, 760) - text = "adsbexchange.com" - draw.text((x, y), text, fill=black, font=head_font) + if text_credit is not None: + draw.rectangle(((658, 762), (800, 782)), fill= white) + (x, y) = (660, 760) + text = text_credit + draw.text((x, y), text, fill=black, font=head_font) #Nearest Airport Header (x, y) = (422, 740) text = "Nearest Airport" diff --git a/planeClass.py b/planeClass.py index 83bb786..be40441 100644 --- a/planeClass.py +++ b/planeClass.py @@ -41,14 +41,21 @@ class Plane: self.track = None self.last_track = None self.circle_history = None + self.type = None if self.config.has_option('DATA', 'DATA_LOSS_MINS'): self.data_loss_mins = self.config.getint('DATA', 'DATA_LOSS_MINS') else: self.data_loss_mins = Plane.main_config.getint('DATA', 'DATA_LOSS_MINS') #Setup Tweepy - if self.config.getboolean('TWITTER', 'ENABLE'): - from defTweet import tweepysetup - self.tweet_api = tweepysetup(self.config) + if self.config.getboolean('TWITTER', 'ENABLE') and Plane.main_config.getboolean('TWITTER', 'ENABLE'): + import tweepy + twitter_app_auth = tweepy.OAuthHandler(Plane.main_config.get('TWITTER', 'CONSUMER_KEY'), Plane.main_config.get('TWITTER', 'CONSUMER_SECRET')) + twitter_app_auth.set_access_token(config.get('TWITTER', 'ACCESS_TOKEN'), config.get('TWITTER', 'ACCESS_TOKEN_SECRET')) + self.tweet_api = tweepy.API(twitter_app_auth, wait_on_rate_limit=True) + try: + self.latest_tweet_id = self.tweet_api.user_timeline(count = 1)[0] + except IndexError: + self.latest_tweet_id = None #Setup PushBullet if self.config.getboolean('PUSHBULLET', 'ENABLE'): from pushbullet import Pushbullet @@ -57,7 +64,7 @@ class Plane: def run_opens(self, ac_dict): #Parse OpenSky Vector from colorama import Fore, Back, Style - self.printheader("head") + self.print_header("BEGIN") #print (Fore.YELLOW + "OpenSky Sourced Data: ", ac_dict) try: self.__dict__.update({'icao' : ac_dict.icao24.upper(), 'callsign' : ac_dict.callsign, 'latitude' : ac_dict.latitude, 'longitude' : ac_dict.longitude, 'on_ground' : bool(ac_dict.on_ground), 'squawk' : ac_dict.squawk, 'track' : float(ac_dict.heading)}) @@ -72,14 +79,14 @@ class Plane: except ValueError as e: print("Got data but some data is invalid!") print(e) - self.printheader("foot") + self.print_header("END") else: self.feeding = True self.run_check() def run_adsbx_v1(self, ac_dict): #Parse ADBSX V1 Vector from colorama import Fore, Back, Style - self.printheader("head") + self.print_header("BEGIN") #print (Fore.YELLOW +"ADSBX Sourced Data: ", ac_dict, Style.RESET_ALL) try: #postime is divided by 1000 to get seconds from milliseconds, from timestamp expects secs. @@ -92,7 +99,7 @@ class Plane: print("Got data but some data is invalid!") print(e) print (Fore.YELLOW +"ADSBX Sourced Data: ", ac_dict, Style.RESET_ALL) - self.printheader("foot") + self.print_header("END") else: self.feeding = True self.run_check() @@ -100,7 +107,7 @@ class Plane: def run_adsbx_v2(self, ac_dict): #Parse ADBSX V2 Vector from colorama import Fore, Back, Style - self.printheader("head") + self.print_header("BEGIN") print(ac_dict) try: self.__dict__.update({'icao' : ac_dict['hex'].upper(), 'latitude' : float(ac_dict['lat']), 'longitude' : float(ac_dict['lon']), 'speed': ac_dict['gs']}) @@ -116,6 +123,8 @@ class Plane: self.on_ground = True if ac_dict.get('flight') is not None: self.callsign = ac_dict.get('flight').strip() + else: + self.callsign = None if ac_dict.get('dbFlags') is not None: self.db_flags = ac_dict['dbFlags'] if 'nav_modes' in ac_dict: @@ -142,7 +151,7 @@ class Plane: print("Got data but some data is invalid!") print(e) print (Fore.YELLOW +"ADSBX Sourced Data: ", ac_dict, Style.RESET_ALL) - self.printheader("foot") + self.print_header("END") else: #Error Handling for bad data, sometimes it would seem to be ADSB Decode error if (not self.on_ground) and self.speed <= 10: @@ -169,13 +178,19 @@ class Plane: ] output = list(filter(None, output)) return tabulate(output, [], 'fancy_grid') - def printheader(self, type): + def print_header(self, note): from colorama import Fore, Back, Style - if type == "head": - header = str("--------- " + self.conf_file_path + " ---------------------------- ICAO: " + self.icao + " ---------------------------------------") - elif type == "foot": - header = "----------------------------------------------------------------------------------------------------" - print(Back.MAGENTA + header[0:100] + Style.RESET_ALL) + if note == "BEGIN": + header = f"---BEGIN---------{self.conf_file_path}" + elif note == "END": + header = f"---END" + remaning_len = 85 - len(header) + for x in range(0, remaning_len): + header += "-" + header += f"ICAO: {self.icao}---" + if note =="END": + header += Style.RESET_ALL + "\n" + print(Back.MAGENTA + header + Style.RESET_ALL) def get_time_since(self, datetime_obj): if datetime_obj != None: time_since = datetime.now() - datetime_obj @@ -186,7 +201,7 @@ class Plane: if self.config.has_option('MAP', 'OVERLAYS'): overlays = self.config.get('MAP', 'OVERLAYS') else: - overlays = "" + overlays = "null" return overlays def route_info(self): from lookup_route import lookup_route, clean_data @@ -243,7 +258,7 @@ class Plane: return route_to def run_empty(self): - self.printheader("head") + self.print_header("BEGIN") self.feeding = False self.run_check() def run_check(self): @@ -352,17 +367,19 @@ class Plane: area = f"{municipality}, {state}" location_string = (f"{area}, {country_code}") print (Fore.GREEN + "Country Code:", country_code, "State:", state, "Municipality:", municipality + Style.RESET_ALL) - title_switch = { - "reg": self.reg, - "callsign": self.callsign, - "icao": self.icao, - } + dynamic_title = self.callsign or self.reg or self.icao #Set Discord Title if self.config.getboolean('DISCORD', 'ENABLE'): - self.dis_title = (title_switch.get(self.config.get('DISCORD', 'TITLE')) or "NA").strip() if self.config.get('DISCORD', 'TITLE') in title_switch.keys() else self.config.get('DISCORD', 'TITLE') + if self.config.get('DISCORD', 'TITLE') in ["DYNAMIC", "callsign"]: + self.dis_title = dynamic_title + else: + self.dis_title = self.config.get('DISCORD', 'TITLE') #Set Twitter Title if self.config.getboolean('TWITTER', 'ENABLE'): - self.twitter_title = (title_switch.get(self.config.get('TWITTER', 'TITLE')) or "NA") if self.config.get('TWITTER', 'TITLE') in title_switch.keys() else self.config.get('TWITTER', 'TITLE') + if self.config.get('TWITTER', 'TITLE') in ["DYNAMIC", "callsign"]: + self.twitter_title = dynamic_title + else: + self.twitter_title = self.config.get('TWITTER', 'TITLE') #Takeoff and Land Notification if self.tookoff or self.landed: route_to = None @@ -392,26 +409,37 @@ class Plane: self.takeoff_time = None elif self.landed: landed_time_msg = None - message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "") + landed_time = None + if self.icao != "A835AF": + message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "") + dirty_message = None + else: + message = (f"{type_header} {location_string}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "") + dirty_message = (f"{type_header} {location_string}.") + ("" if route_to is None else f" {route_to}.") + ((f" {landed_time_msg}") if landed_time_msg != None else "") print (message) #Google Map or tar1090 screenshot - if self.config.get('MAP', 'OPTION') == "GOOGLESTATICMAP": + if Plane.main_config.get('MAP', 'OPTION') == "GOOGLESTATICMAP": from defMap import getMap getMap((municipality + ", " + state + ", " + country_code), self.map_file_name) - elif self.config.get('MAP', 'OPTION') == "ADSBX": + elif Plane.main_config.get('MAP', 'OPTION') == "ADSBX": from defSS import get_adsbx_screenshot - url_params = f"icao={self.icao}&zoom=9&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays=" + self.get_adsbx_map_overlays() get_adsbx_screenshot(self.map_file_name, url_params) from modify_image import append_airport - append_airport(self.map_file_name, nearest_airport_dict) + text_credit = self.config.get('MAP', 'TEXT_CREDIT') if self.config.has_option('MAP', 'TEXT_CREDIT') else None + append_airport(self.map_file_name, nearest_airport_dict, text_credit) else: raise ValueError("Map option not set correctly in this planes conf") + #Telegram + if self.config.has_section('TELEGRAM') and self.config.getboolean('TELEGRAM', 'ENABLE'): + from defTelegram import sendTeleg + photo = open(self.map_file_name, "rb") + sendTeleg(photo, message, self.config) #Discord if self.config.getboolean('DISCORD', 'ENABLE'): - dis_message = f"{self.dis_title} {message}".strip() + dis_message = f"{self.dis_title} {message}".strip() if dirty_message is None else f"{self.dis_title} {dirty_message}".strip() role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None - sendDis(dis_message, self.config, self.map_file_name, role_id = role_id) + sendDis(dis_message, self.config, role_id, self.map_file_name) #PushBullet if self.config.getboolean('PUSHBULLET', 'ENABLE'): with open(self.map_file_name, "rb") as pic: @@ -420,12 +448,50 @@ class Plane: self.pb_channel.push_file(**map_data) #Twitter if self.config.getboolean('TWITTER', 'ENABLE'): - twitter_media_map_obj = self.tweet_api.media_upload(self.map_file_name) - alt_text = f"Reg: {self.reg} On Ground: {str(self.on_ground)} Alt: {str(self.alt_ft)} Last Contact: {str(time_since_contact)} Trigger: {trigger_type}" - self.tweet_api.create_media_metadata(media_id= twitter_media_map_obj.media_id, alt_text= alt_text) - self.latest_tweet_id = self.tweet_api.update_status(status = ((self.twitter_title + " " + message).strip()), media_ids=[twitter_media_map_obj.media_id]).id + import tweepy + try: + twitter_media_map_obj = self.tweet_api.media_upload(self.map_file_name) + alt_text = f"Reg: {self.reg} On Ground: {str(self.on_ground)} Alt: {str(self.alt_ft)} Last Contact: {str(time_since_contact)} Trigger: {trigger_type}" + self.tweet_api.create_media_metadata(media_id= twitter_media_map_obj.media_id, alt_text= alt_text) + self.latest_tweet_id = self.tweet_api.update_status(status = ((self.twitter_title + " " + message).strip()), media_ids=[twitter_media_map_obj.media_id]).id + except tweepy.errors.TweepyException as e: + print(e) + raise Exception(self.icao) from e + #Meta + if self.config.has_option('META', 'ENABLE') and self.config.getboolean('META', 'ENABLE'): + from meta_toolkit import post_to_meta_both + post_to_meta_both(self.config.get("META", "FB_PAGE_ID"), self.config.get("META", "IG_USER_ID"), self.map_file_name, message, self.config.get("META", "ACCESS_TOKEN")) os.remove(self.map_file_name) if self.landed: + if self.known_to_airport is not None and self.nearest_from_airport is not None and self.known_to_airport != self.nearest_from_airport: + from defAirport import get_airport_by_icao + from geopy.distance import geodesic + known_to_airport = get_airport_by_icao(self.known_to_airport) + nearest_from_airport = get_airport_by_icao(self.nearest_from_airport) + from_coord = (nearest_from_airport['latitude_deg'], nearest_from_airport['longitude_deg']) + to_coord = (known_to_airport['latitude_deg'], known_to_airport['longitude_deg']) + distance_mi = float(geodesic(from_coord, to_coord).mi) + distance_nm = distance_mi / 1.150779448 + distance_message = f"{'{:,}'.format(round(distance_mi))} mile ({'{:,}'.format(round(distance_nm))} NM) flight from {nearest_from_airport['iata_code']} to {nearest_airport_dict['iata_code']}\n" + else: + distance_message = "" + if landed_time is not None and self.type is not None: + print("Running fuel info calc") + flight_time_min = landed_time.total_seconds() / 60 + from fuel_calc import fuel_calculation, fuel_message + fuel_info = fuel_calculation(self.type, flight_time_min) + if fuel_info is not None: + fuel_message = fuel_message(fuel_info) + if self.config.getboolean('DISCORD', 'ENABLE'): + dis_message = f"{self.dis_title} {distance_message} \nFlight Fuel Info ```{fuel_message}```".strip() + role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None + sendDis(dis_message, self.config, role_id) + if self.config.getboolean('TWITTER', 'ENABLE'): + try: + self.latest_tweet_id = self.tweet_api.update_status(status = ((self.twitter_title + " " + distance_message + " " + fuel_message).strip()), in_reply_to_status_id = self.latest_tweet_id).id + except tweepy.errors.TweepyException as e: + print(e) + raise Exception(self.icao) from e self.latest_tweet_id = None self.recheck_route_time = None self.known_to_airport = None @@ -436,13 +502,19 @@ class Plane: route_to = self.route_info() if route_to != None: print(route_to) + #Telegram + if self.config.has_section('TELEGRAM') and self.config.getboolean('TELEGRAM', 'ENABLE'): + message = f"{self.dis_title} {route_to}".strip() + photo = open(self.map_file_name, "rb") + from defTelegram import sendTeleg + sendTeleg(photo, message, self.config) #Discord if self.config.getboolean('DISCORD', 'ENABLE'): dis_message = f"{self.dis_title} {route_to}".strip() role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None - sendDis(dis_message, self.config, role_id = role_id) + sendDis(dis_message, self.config, role_id) #Twitter - if self.config.getboolean('TWITTER', 'ENABLE'): + if self.config.getboolean('TWITTER', 'ENABLE') and self.icao == 'A835AF': #tweet = self.tweet_api.user_timeline(count = 1)[0] self.latest_tweet_id = self.tweet_api.update_status(status = f"{self.twitter_title} {route_to}".strip(), in_reply_to_status_id = self.latest_tweet_id).id @@ -469,15 +541,15 @@ class Plane: squawk_message = (f"{self.dis_title} Squawking {self.last_emergency[1]} {emergency_squawks[self.squawk]}").strip() print(squawk_message) #Google Map or tar1090 screenshot - if self.config.get('MAP', 'OPTION') == "GOOGLESTATICMAP": + if Plane.main_config.get('MAP', 'OPTION') == "GOOGLESTATICMAP": getMap((municipality + ", " + state + ", " + country_code), self.map_file_name) - if self.config.get('MAP', 'OPTION') == "ADSBX": + if Plane.main_config.get('MAP', 'OPTION') == "ADSBX": from defSS import get_adsbx_screenshot url_params = f"icao={self.icao}&zoom=9&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays=" + self.get_adsbx_map_overlays() get_adsbx_screenshot(self.map_file_name, url_params) if self.config.getboolean('DISCORD', 'ENABLE'): dis_message = (self.dis_title + " " + squawk_message) - sendDis(dis_message, self.config, self.map_file_name) + sendDis(dis_message, self.config, None, self.map_file_name) os.remove(self.map_file_name) #Realizes first time seeing emergency, stores time and type elif self.squawk in emergency_squawks.keys() and not self.emergency_already_triggered and not self.on_ground: @@ -498,7 +570,7 @@ class Plane: from defSS import get_adsbx_screenshot url_params = f"icao={self.icao}&zoom=9&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays={self.get_adsbx_map_overlays()}" get_adsbx_screenshot(self.map_file_name, url_params) - sendDis(dis_message, self.config, self.map_file_name) + sendDis(dis_message, self.config, None, self.map_file_name) #elif mode in ["Althold", "VNAV", "LNAV"] and self.sel_nav_alt != None: # sendDis((dis_message + ", Sel Alt. " + str(self.sel_nav_alt) + ", Current Alt. " + str(self.alt_ft)), self.config) else: @@ -509,7 +581,7 @@ class Plane: print("Nav altitude is now", self.sel_nav_alt) if self.config.getboolean('DISCORD', 'ENABLE'): dis_message = (self.dis_title + " Sel. alt. " + str("{:,} ft".format(self.sel_nav_alt))) - sendDis(dis_message,self.config) + sendDis(dis_message, self.config) #Circling if self.last_track is not None: import time @@ -543,15 +615,138 @@ class Plane: #rp = (points.representative_point()) #A represenative point, not centroid, print(cent) #print(rp) - distance_to_centroid = geodesic(aircraft_coords, cent.coords).mi + distance_to_centroid = round(geodesic(aircraft_coords, cent.coords).mi, 2) print(f"Distance to centroid of circling coordinates {distance_to_centroid} miles") if distance_to_centroid <= 15: print("Within 15 miles of centroid, CIRCLING") + #Finds Nearest Airport from defAirport import getClosestAirport nearest_airport_dict = getClosestAirport(self.latitude, self.longitude, ["small_airport", "medium_airport", "large_airport"]) from calculate_headings import calculate_from_bearing, calculate_cardinal from_bearing = calculate_from_bearing((float(nearest_airport_dict['latitude_deg']), float(nearest_airport_dict['longitude_deg'])), (self.latitude, self.longitude)) cardinal = calculate_cardinal(from_bearing) + #Finds Nearest TFR or in TFR + from shapely.geometry import MultiPoint + from geopy.distance import geodesic + from shapely.geometry.polygon import Polygon + from shapely.geometry import Point + import requests, json + tfr_url = Plane.main_config.get("TFRS", "URL") + response = requests.get(tfr_url, timeout=30) + tfrs = json.loads(response.text) + closest_tfr = None + in_tfr = None + for tfr in tfrs: + if in_tfr is not None: + break + elif tfr['details'] is not None and 'shapes' in tfr['details'].keys(): + for index, shape in enumerate(tfr['details']['shapes']): + if 'txtName' not in shape.keys(): + shape['txtName'] = 'shape_'+str(index) + polygon = None + if shape['type'] == "poly": + points = shape['points'] + elif shape['type'] == "circle": + from functools import partial + import pyproj + from shapely.ops import transform + from shapely.geometry import Point + proj_wgs84 = pyproj.Proj('+proj=longlat +datum=WGS84') + def geodesic_point_buffer(lat, lon, km): + # Azimuthal equidistant projection + aeqd_proj = '+proj=aeqd +lat_0={lat} +lon_0={lon} +x_0=0 +y_0=0' + project = partial( + pyproj.transform, + pyproj.Proj(aeqd_proj.format(lat=lat, lon=lon)), + proj_wgs84) + buf = Point(0, 0).buffer(km * 1000) # distance in metres + return transform(project, buf).exterior.coords[:] + radius_km = float(shape['radius']) * 1.852 + b = geodesic_point_buffer(shape['lat'], shape['lon'], radius_km) + points = [] + for coordinate in b: + points.append([coordinate[1], coordinate[0]]) + elif shape['type'] in ["polyarc", "polyexclude"]: + points = shape['all_points'] + aircraft_location = Point(self.latitude, self.longitude) + if polygon is None: + polygon = Polygon(points) + if polygon.contains(aircraft_location): + in_tfr = {'info': tfr, 'closest_shape_name' : shape['txtName']} + break + else: + point_dists = [] + for point in points: + from geopy.distance import geodesic + point = tuple(point) + point_dists.append(float((geodesic((self.latitude, self.longitude), point).mi))) + distance = min(point_dists) + if closest_tfr is None: + closest_tfr = {'info': tfr, 'closest_shape_name' : shape['txtName'], 'distance' : round(distance)} + elif distance < closest_tfr['distance']: + closest_tfr = {'info': tfr, 'closest_shape_name' : shape['txtName'], 'distance' : round(distance)} + if in_tfr is not None: + for shape in in_tfr['info']['details']['shapes']: + if shape['txtName'] == in_tfr['closest_shape_name']: + valDistVerUpper, valDistVerLower = int(shape['valDistVerUpper']), int(shape['valDistVerLower']) + print("In TFR based off location checking alt next", in_tfr) + break + if not (self.alt_ft >= valDistVerLower and self.alt_ft <= valDistVerUpper): + print("But not in alt of TFR") + closest_tfr = in_tfr + closest_tfr['distance'] = 0 + in_tfr = None + if in_tfr is None: + print("Closest TFR", closest_tfr) + #Generate Map + import staticmaps + context = staticmaps.Context() + context.set_tile_provider(staticmaps.tile_provider_OSM) + if in_tfr is not None: + shapes = in_tfr['info']['details']['shapes'] + else: + shapes = closest_tfr['info']['details']['shapes'] + def draw_poly(context, pairs): + pairs.append(pairs[0]) + context.add_object( + staticmaps.Area( + [staticmaps.create_latlng(lat, lng) for lat, lng in pairs], + fill_color=staticmaps.parse_color("#FF000033"), + width=2, + color=staticmaps.parse_color("#8B0000"), + ) + ) + return context + for shape in shapes: + if shape['type'] == "poly": + pairs = shape['points'] + context = draw_poly(context, pairs) + elif shape['type'] == "polyarc" or shape['type'] == "polyexclude": + pairs = shape['all_points'] + context = draw_poly(context, pairs) + elif shape['type'] =="circle": + center = [shape['lat'], shape['lon']] + center1 = staticmaps.create_latlng(center[0], center[1]) + context.add_object(staticmaps.Circle(center1, (float(shape['radius']) * 1.852), fill_color=staticmaps.parse_color("#FF000033"), color=staticmaps.parse_color("#8B0000"), width=2)) + context.add_object(staticmaps.Marker(center1, color=staticmaps.RED)) + def tfr_image(context, aircraft_coords): + from PIL import Image + heading = self.track + heading *= -1 + im = Image.open('./dependencies/ac.png') + im_rotate = im.rotate(heading, resample=Image.BICUBIC) + import tempfile + rotated_file = f"{tempfile.gettempdir()}/rotated_ac.png" + im_rotate.save(rotated_file) + pos = staticmaps.create_latlng(aircraft_coords[0], aircraft_coords[1]) + marker = staticmaps.ImageMarker(pos, rotated_file, origin_x=35, origin_y=35) + context.add_object(marker) + image = context.render_cairo(1000, 1000) + os.remove(rotated_file) + tfr_map_filename = f"{tempfile.gettempdir()}/{self.icao}_TFR_.png" + image.write_to_png(tfr_map_filename) + return tfr_map_filename + from defSS import get_adsbx_screenshot url_params = f"icao={self.icao}&zoom=10&largeMode=2&hideButtons&hideSidebar&mapDim=0&overlays={self.get_adsbx_map_overlays()}" get_adsbx_screenshot(self.map_file_name, url_params) @@ -559,20 +754,46 @@ class Plane: if "touchngo" in self.circle_history.keys(): message = f"Doing touch and goes at {nearest_airport_dict['icao']}" else: - message = f"Circling over {nearest_airport_dict['icao']} at {self.alt_ft}ft" + message = f"Circling over {nearest_airport_dict['icao']} at {self.alt_ft}ft." else: - message = f"Circling {round(nearest_airport_dict['distance_mi'], 2)}mi {cardinal} of {nearest_airport_dict['icao']}, {nearest_airport_dict['name']} at {self.alt_ft}ft" + message = f"Circling {round(nearest_airport_dict['distance_mi'], 2)}mi {cardinal} of {nearest_airport_dict['icao']}, {nearest_airport_dict['name']} at {self.alt_ft}ft. " + tfr_map_filename = None + if in_tfr is not None: + message += f" Inside TFR {in_tfr['info']['NOTAM']}, a TFR for {in_tfr['info']['Type'].title()}" + tfr_map_filename = tfr_image(context, (self.latitude, self.longitude)) + elif in_tfr is None and "distance" in closest_tfr.keys() and closest_tfr["distance"] <= 20: + message += f" {closest_tfr['distance']} miles from TFR {closest_tfr['info']['NOTAM']}, a TFR for {closest_tfr['info']['Type']}" + tfr_map_filename = tfr_image(context, (self.latitude, self.longitude)) + elif in_tfr is None and "distance" not in closest_tfr.keys(): + message += f" near TFR {closest_tfr['info']['NOTAM']}, a TFR for {closest_tfr['info']['Type']}" + raise Exception(message) + print(message) + #Telegram + if self.config.has_section('TELEGRAM') and self.config.getboolean('TELEGRAM', 'ENABLE'): + photo = open(self.map_file_name, "rb") + from defTelegram import sendTeleg + sendTeleg(photo, message, self.config) if self.config.getboolean('DISCORD', 'ENABLE'): role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None - sendDis(message, self.config, self.map_file_name, role_id) + if tfr_map_filename is not None: + sendDis(message, self.config, role_id, self.map_file_name, tfr_map_filename) + elif tfr_map_filename is None: + sendDis(message, self.config, role_id, self.map_file_name) if self.config.getboolean('TWITTER', 'ENABLE'): twitter_media_map_obj = self.tweet_api.media_upload(self.map_file_name) - alt_text = f"Distance to centroid: {distance_to_centroid}, Total change: {total_change}" - self.tweet_api.create_media_metadata(media_id= twitter_media_map_obj.media_id, alt_text= alt_text) - tweet = self.tweet_api.user_timeline(count = 1)[0] - self.latest_tweet_id = self.tweet_api.update_status(status = f"{self.twitter_title} {message}".strip(), in_reply_to_status_id = self.latest_tweet_id, media_ids=[twitter_media_map_obj.media_id]).id - + media_ids = [twitter_media_map_obj.media_id] + if tfr_map_filename is not None: + twitter_media_tfr_map_obj = self.tweet_api.media_upload(tfr_map_filename) + media_ids.append(twitter_media_tfr_map_obj.media_id) + elif tfr_map_filename is None: + print("No TFR Map") + tweet = f"{self.twitter_title} {message}".strip() + self.tweet_api.update_status(status = tweet, media_ids=media_ids) + #Meta + if self.config.has_option('META', 'ENABLE') and self.config.getboolean('META', 'ENABLE'): + from meta_toolkit import post_to_meta_both + post_to_meta_both(self.config.get("META", "FB_PAGE_ID"), self.config.get("META", "IG_USER_ID"), self.map_file_name, message, self.config.get("META", "ACCESS_TOKEN")) self.circle_history['triggered'] = True elif abs(total_change) <= 360 and self.circle_history["triggered"]: print("No Longer Circling, trigger cleared") @@ -600,7 +821,7 @@ class Plane: hours, remainder = divmod(elapsed_time.total_seconds(), 3600) minutes, seconds = divmod(remainder, 60) print((f"Time Since Take off {int(hours)} Hours : {int(minutes)} Mins : {int(seconds)} Secs")) - self.printheader("foot") + self.print_header("END") def check_new_ras(self, ras): for ra in ras: if self.recent_ra_types == {} or ra['acas_ra']['advisory'] not in self.recent_ra_types.keys(): @@ -627,7 +848,7 @@ class Plane: from defDiscord import sendDis dis_message = f"{self.dis_title} {ra_message}" role_id = self.config.get('DISCORD', 'ROLE_ID') if self.config.has_option('DISCORD', 'ROLE_ID') else None - sendDis(dis_message, self.config, self.map_file_name, role_id = role_id) + sendDis(dis_message, self.config, role_id, self.map_file_name) #if twitter def expire_ra_types(self): if self.recent_ra_types != {}: