Skip to content

Commit

Permalink
Merge pull request #25 from joergschultzelutter/22-add-static-map-ima…
Browse files Browse the repository at this point in the history
…ge-to-outgoing-posmsg

added posmsg location image, updates to airport/repeater databases
  • Loading branch information
joergschultzelutter authored Apr 13, 2024
2 parents 6e5487d + a9cdc30 commit 77e8d02
Show file tree
Hide file tree
Showing 10 changed files with 316 additions and 58 deletions.
21 changes: 3 additions & 18 deletions docs/DEPENDENCIES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,12 @@

## Python Library Dependencies

The following Python packages need to be installed (see also [requirements.txt](../requirements.txt)):

- [activesoup](https://github.com/jelford/activesoup) required: version 0.3.0 or greater
- [apscheduler](https://github.com/agronholm/apscheduler)
- [aprslib](https://github.com/rossengeorgiev/aprs-python)
- [beautifulsoup4](https://www.crummy.com/software/BeautifulSoup/)
- [expiringdict](https://pypi.org/project/expiringdict/)
- [geopy](https://github.com/geopy/geopy)
- [iso3166](https://github.com/deactivated/python-iso3166)
- [maidenhead](https://github.com/space-physics/maidenhead)
- [pymgrs](https://github.com/aydink/pymgrs) Note: this is __not__ a pip package; download the mgrs.py file and save it in the src directory
- [requests](https://github.com/psf/requests)
- [skyfield](https://github.com/skyfielders/python-skyfield)
- [timezonefinder](https://github.com/MrMinimal64/timezonefinder)
- [unidecode](https://github.com/avian2/unidecode)
- [us](https://github.com/unitedstates/python-us)
- [utm](https://github.com/Turbo87/utm)
- [xmltodict](https://github.com/martinblech/xmltodict)
All dependencies are included in [requirements.txt](../requirements.txt). Install via ```pip install -r requirements.txt```

If you install MPAD and its components on a Raspberry Pi, the skyfield package requires you to install ```apt-get install libatlas-base-dev``` as a separate dependency.

Dependent on your OS' flavor, you may be required to install the following additional packages: ```apt-get install libgeos-dev libopenjp2-7```

## API Dependencies

- [aprs.fi](https://aprs.fi/page/api) - thank you Hessu!
Expand Down
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,6 @@ unidecode>=1.3.2
us>=2.0.2
utm>=0.7.0
xmltodict>=0.12.0
pillow<10.0.0
py-staticmaps==0.4.0
mgrs>=1.4.6
9 changes: 4 additions & 5 deletions src/airport_data_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
import math
import logging
from utility_modules import check_if_file_exists, build_full_pathname
from mpad_config import mpad_airport_stations_filename

# icao https://www.aviationweather.gov/docs/metar/stations.txt

Expand All @@ -36,7 +37,7 @@


def read_local_airport_data_file(
airport_stations_filename: str = "airport_stations.txt",
airport_stations_filename: str = mpad_airport_stations_filename,
):
"""
Imports the ICAO/IATA data from a local file. Creates dictionaries for
Expand All @@ -47,7 +48,6 @@ def read_local_airport_data_file(
airport_stations_filename : 'str'
local file that is to be parsed. File format:
see https://www.aviationweather.gov/docs/metar/stations.txt
default filename: "airport_stations.txt"
Returns
=======
Expand Down Expand Up @@ -326,7 +326,7 @@ def validate_icao(icao_code: str):


def update_local_airport_stations_file(
airport_stations_filename: str = "airport_stations.txt",
airport_stations_filename: str = mpad_airport_stations_filename,
):
"""
Imports the ICAO/IATA data from the web and saves it to a local file.
Expand All @@ -336,7 +336,6 @@ def update_local_airport_stations_file(
airport_stations_filename : 'str'
This local file will hold the content
from https://www.aviationweather.gov/docs/metar/stations.txt.
Default filename is "airport_stations.txt"
Returns
=======
Expand All @@ -345,7 +344,7 @@ def update_local_airport_stations_file(
"""

# This is the fixed name of the URL Source that we are going to download
file_url = "https://www.aviationweather.gov/docs/metar/stations.txt"
file_url = "https://weather.ral.ucar.edu/surface/stations.txt"
success: bool = False

absolute_path_filename = build_full_pathname(file_name=airport_stations_filename)
Expand Down
123 changes: 113 additions & 10 deletions src/email_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import smtplib
import imaplib
from email.message import EmailMessage
from email.utils import make_msgid
import re
import datetime
import mpad_config
Expand All @@ -32,15 +33,19 @@
convert_latlon_to_utm,
convert_latlon_to_mgrs,
)
from staticmap import render_png_map
from utility_modules import read_program_config

logging.basicConfig(
level=logging.DEBUG, format="%(asctime)s %(module)s -%(levelname)s- %(message)s"
)
logger = logging.getLogger(__name__)

# The following two variables define the templates for the outgoing email
# The first one is simple plain text whereas the second one is HTML
# The following three variables define the templates for the outgoing email
# Template 1 = plain text (also used as fallback format)
# Templates 2 and 3 = html data with/without image with user's position
# template without user's position is only used in case
# we were unable to generate the image
#
# YES - I KNOW. Normal people would import this from a file. Welcome to Team Different.

Expand Down Expand Up @@ -71,7 +76,7 @@
Proudly made in the district of Holzminden, Lower Saxony, Germany. 73 de DF1JSL
"""

html_template = """\
html_template_without_image = """\
<h2>Automated email - please do not respond</h2>
<p>APRS position data for <strong>REPLACE_MESSAGECALLSIGN</strong> on the Internet:</p>
<ul>
Expand Down Expand Up @@ -141,6 +146,79 @@
<p>Proudly made in the district of Holzminden, Lower Saxony, Germany. 73 de DF1JSL</p>
"""

html_template_with_image = """\
<h2>Automated email - please do not respond</h2>
<p>APRS position data for <strong>REPLACE_MESSAGECALLSIGN</strong> on the Internet:</p>
<ul>
<li><a href="REPLACE_APRSDOTFI" target="_blank" rel="noopener">aprs.fi</a></li>
<li><a href="REPLACE_FINDUDOTCOM" target="_blank" rel="noopener">FindU.com</a></li>
<li><a href="REPLACE_GOOGLEMAPS" target="_blank" rel="noopener">Google Maps</a>&nbsp;</li>
<li><a href="REPLACE_QRZDOTCOM" target="_blank" rel="noopener">QRZ.com</a>&nbsp;</li>
</ul>
<table border="1">
<thead>
<tr style="background-color: #bbbbbb;">
<td><strong>Position details</strong></td>
<td><strong>Values</strong></td>
</tr>
</thead>
<tbody>
<tr>
<td><strong>&nbsp;Maidenhead</strong> Grid Locator</td>
<td>&nbsp;REPLACE_MAIDENHEAD</td>
</tr>
<tr>
<td><strong>&nbsp;DMS</strong> Degrees and Decimal Minutes&nbsp;</td>
<td>&nbsp;REPLACE_DMS</td>
</tr>
<tr>
<td><strong>&nbsp;UTM</strong> Universal Transverse Mercator</td>
<td>&nbsp;REPLACE_UTM</td>
</tr>
<tr>
<td>
<p><strong>&nbsp;MGRS</strong> Military Grid Reference System</p>
<p><strong>&nbsp;USNG</strong> United States National Grid</p>
</td>
<td>&nbsp;REPLACE_MGRS</td>
</tr>
<tr>
<td>
<p><strong>&nbsp;Latitude and Longitude</strong></p>
</td>
<td>&nbsp;REPLACE_LATLON</td>
</tr>
<tr>
<td>
<p><strong>&nbsp;Altitude</strong></p>
</td>
<td>&nbsp;REPLACE_ALTITUDE</td>
</tr>
<tr>
<td>
<p><strong>&nbsp;Last heard on APRS-IS</strong></p>
</td>
<td>&nbsp;REPLACE_LASTHEARD</td>
</tr>
<tr>
<td>
<p><strong>&nbsp;Address data</strong></p>
</td>
<td>
<p>&nbsp;REPLACE_ADDRESS_DATA</p>
</td>
</tr>
</tbody>
</table>
<hr />
<p><center><img src="cid:{image_cid}" /></center></p>
<hr />
<p>This position report was requested by <strong>REPLACE_USERSCALLSIGN</strong> via APRS and was processed by <a href="https://aprs.fi/#!call=a%2FMPAD&amp;timerange=3600&amp;tail=3600" target="_blank" rel="noopener">MPAD (Multi-Purpose APRS Daemon)</a>. Generated at <strong>REPLACE_DATETIME_CREATED</strong></p>
<p>More info on MPAD can be found here: <a href="https://www.github.com/joergschultzelutter/mpad" target="_blank" rel="noopener">https://www.github.com/joergschultzelutter/mpad</a></p>
<hr />
<p>Proudly made in the district of Holzminden, Lower Saxony, Germany. 73 de DF1JSL</p>
"""

mail_subject_template = "APRS Position Report for REPLACE_MESSAGECALLSIGN"


Expand All @@ -162,14 +240,10 @@ def send_email_position_report(response_parameters: dict):
back to the APRS user (does not contain any email content)
"""

# get the required email data from our data pool
smtpimap_email_address = response_parameters["smtpimap_email_address"]
smtpimap_email_password = response_parameters["smtpimap_email_password"]

# copy the templates
plaintext_message = plaintext_template
html_message = html_template
subject_message = mail_subject_template

latitude = response_parameters["latitude"]
longitude = response_parameters["longitude"]
altitude = response_parameters["altitude"]
Expand All @@ -178,6 +252,19 @@ def send_email_position_report(response_parameters: dict):
users_callsign = response_parameters["users_callsign"]
mail_recipient = response_parameters["mail_recipient"]

# Now try to generate an image map of the user's position...
html_image = render_png_map(aprs_latitude=latitude, aprs_longitude=longitude)
# ... and now choose the template according to whether we happen to
# have been able to generate an image or not
html_message = (
html_template_with_image if html_image else html_template_without_image
)

# copy the remaining templates
plaintext_message = plaintext_template
subject_message = mail_subject_template

# generate the time stamp
lasttime = response_parameters["lasttime"]
if not isinstance(lasttime, datetime.datetime):
lasttime = datetime.datetime.min
Expand Down Expand Up @@ -311,7 +398,21 @@ def send_email_position_report(response_parameters: dict):
msg["From"] = f"MPAD Multi-Purpose APRS Daemon <{smtpimap_email_address}>"
msg["To"] = mail_recipient
msg.set_content(plaintext_message)
msg.add_alternative(html_message, subtype="html")

# Image present? Then encode it properly
if html_image:
image_cid = make_msgid()
msg.add_alternative(
html_message.format(image_cid=image_cid[1:-1]), subtype="html"
)
x = msg.get_payload()

msg.get_payload()[1].add_related(
html_image, maintype="image", subtype="png", cid=image_cid
)
else:
# otherwise, send the HTML content without an image
msg.add_alternative(html_message, subtype="html")

success, output_message = send_message_via_snmp(
smtpimap_email_address=smtpimap_email_address,
Expand Down Expand Up @@ -369,7 +470,9 @@ def imap_garbage_collector(smtpimap_email_address: str, smtpimap_email_password:
# typ, dat = imap.list() # get list of mailboxes
typ, dat = imap.select(mailbox=mpad_config.mpad_imap_mailbox_name)
if typ == "OK":
logger.info(msg=f"IMAP folder SELECT for {mpad_config.mpad_imap_mailbox_name} successful")
logger.info(
msg=f"IMAP folder SELECT for {mpad_config.mpad_imap_mailbox_name} successful"
)
typ, msgnums = imap.search(None, "ALL", query_parms)
if typ == "OK":
for num in msgnums[0].split():
Expand Down
16 changes: 10 additions & 6 deletions src/geo_conversion_modules.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@

import utm
import maidenhead
from mgrs import MGRStoLL, LLtoMGRS

import mgrs as mg
from math import radians, cos, sin, asin, sqrt, atan2, degrees
import logging

Expand Down Expand Up @@ -176,8 +177,9 @@ def convert_latlon_to_mgrs(latitude: float, longitude: float):
mgrs_coordinates: 'str'
MGRS coordinates for the given set of lat/lon coordinates
"""
m = mg.MGRS()

mgrs_coordinates: str = LLtoMGRS(latitude, longitude)
mgrs_coordinates: str = m.toMGRS(latitude=latitude, longitude=longitude)
return mgrs_coordinates


Expand All @@ -201,9 +203,11 @@ def convert_mgrs_to_latlon(mgrs_coordinates: str, output_precision: int = 6):
Longitude value
"""

response = MGRStoLL(mgrs_coordinates)
latitude = round(response["lat"], output_precision)
longitude = round(response["lon"], output_precision)
m = mg.MGRS()
latitude, longitude = m.toLatLon(MGRS=mgrs_coordinates)
latitude = round(latitude, output_precision)
longitude = round(longitude, output_precision)

return latitude, longitude


Expand Down Expand Up @@ -441,7 +445,7 @@ def haversine(
logger.info(convert_latlon_to_maidenhead(51.838720, 08.326819))
logger.info(convert_maidenhead_to_latlon("JO41du91"))

logger.info(convert_latlon_to_mgrs(51.838720, 08.326819))
#logger.info(convert_latlon_to_mgrs(51.838720, 08.326819))
logger.info(convert_mgrs_to_latlon("32UMC5362043315"))

logger.info(convert_latlon_to_dms(51.838720, 08.326819))
Expand Down
10 changes: 9 additions & 1 deletion src/mpad_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
#
# Program version
#
mpad_version: str = "0.43"
mpad_version: str = "0.50"
#
###########################
# Constants, do not change#
Expand Down Expand Up @@ -291,3 +291,11 @@
# ">Multi-Purpose APRS Daemon",
]
#
# Several file names that are used throughout the program and act as local databases
mpad_airport_stations_filename = "airport_stations.txt"
mpad_tle_amateur_satellites_filename = "tle_amateur_satellites.txt"
mpad_satellite_frequencies_filename = "satellite_frequencies.csv"
mpad_hearham_raw_data_filename = "hearham_raw_data.json"
mpad_repeatermap_raw_data_filename = "repeatermap_raw_data.json"
mpad_repeater_data_filename = "mpad_repeater_data.json"
mpad_satellite_data_filename = "mpad_satellite_data.json"
Loading

0 comments on commit 77e8d02

Please sign in to comment.