ShadowFinder 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shadowfinder-0.1.0/LICENSE +21 -0
- shadowfinder-0.1.0/PKG-INFO +35 -0
- shadowfinder-0.1.0/README.md +7 -0
- shadowfinder-0.1.0/pyproject.toml +38 -0
- shadowfinder-0.1.0/shadowfinder/__init__.py +0 -0
- shadowfinder-0.1.0/shadowfinder/cli.py +49 -0
- shadowfinder-0.1.0/shadowfinder/main.py +10 -0
- shadowfinder-0.1.0/shadowfinder/shadowfinder.py +88 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Stichting Bellingcat
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: ShadowFinder
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Find possible locations of shadows.
|
|
5
|
+
Home-page: https://github.com/bellingcat/ShadowFinder
|
|
6
|
+
License: MIT
|
|
7
|
+
Keywords: shadow,finder,locator,map
|
|
8
|
+
Author: Bellingcat
|
|
9
|
+
Requires-Python: >=3.9,<3.13
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Intended Audience :: Science/Research
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
20
|
+
Requires-Dist: basemap (>=1.4,<2.0)
|
|
21
|
+
Requires-Dist: fire (>=0.5,<0.6)
|
|
22
|
+
Requires-Dist: matplotlib (>=3.8,<4.0)
|
|
23
|
+
Requires-Dist: suncalc (>=0.1.3,<0.2.0)
|
|
24
|
+
Project-URL: Bug Tracker, https://github.com/bellingcat/ShadowFinder/issues
|
|
25
|
+
Project-URL: Repository, https://github.com/bellingcat/ShadowFinder
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# ShadowFinder
|
|
29
|
+
|
|
30
|
+
A lightweight tool and Google Colab notebook for estimating the points on the Earth's surface where a shadow of a particular length could occur, for geolocation purposes.
|
|
31
|
+
|
|
32
|
+
Using an object's height, the lenth of its shadow, the date and the time, ShadowFinder estimates the possible locations where that shadow could occur.
|
|
33
|
+
|
|
34
|
+
[Try out ShadowFinder on Google Colab now](https://colab.research.google.com/github/Bellingcat/ShadowFinder/blob/main/ShadowFinderColab.ipynb)
|
|
35
|
+
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# ShadowFinder
|
|
2
|
+
|
|
3
|
+
A lightweight tool and Google Colab notebook for estimating the points on the Earth's surface where a shadow of a particular length could occur, for geolocation purposes.
|
|
4
|
+
|
|
5
|
+
Using an object's height, the lenth of its shadow, the date and the time, ShadowFinder estimates the possible locations where that shadow could occur.
|
|
6
|
+
|
|
7
|
+
[Try out ShadowFinder on Google Colab now](https://colab.research.google.com/github/Bellingcat/ShadowFinder/blob/main/ShadowFinderColab.ipynb)
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "ShadowFinder"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Find possible locations of shadows."
|
|
5
|
+
authors = ["Bellingcat"]
|
|
6
|
+
license = "MIT License"
|
|
7
|
+
readme = "README.md"
|
|
8
|
+
repository = "https://github.com/bellingcat/ShadowFinder"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Intended Audience :: Developers",
|
|
11
|
+
"Intended Audience :: Science/Research",
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"License :: OSI Approved :: MIT License",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Topic :: Scientific/Engineering :: Visualization",
|
|
16
|
+
]
|
|
17
|
+
keywords=["shadow", "finder", "locator", "map"]
|
|
18
|
+
|
|
19
|
+
[tool.poetry.urls]
|
|
20
|
+
"Bug Tracker" = "https://github.com/bellingcat/ShadowFinder/issues"
|
|
21
|
+
|
|
22
|
+
[tool.poetry.scripts]
|
|
23
|
+
shadowfinder = "shadowfinder.main:main_entrypoint"
|
|
24
|
+
|
|
25
|
+
[tool.poetry.dependencies]
|
|
26
|
+
python = ">=3.9,<3.13"
|
|
27
|
+
matplotlib = "^3.8"
|
|
28
|
+
basemap = "^1.4"
|
|
29
|
+
suncalc = "^0.1.3"
|
|
30
|
+
fire = "^0.5"
|
|
31
|
+
|
|
32
|
+
[tool.poetry.group.dev.dependencies]
|
|
33
|
+
black = "^24.2.0"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
[build-system]
|
|
37
|
+
requires = ["poetry-core"]
|
|
38
|
+
build-backend = "poetry.core.masonry.api"
|
|
File without changes
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from datetime import datetime, timezone
|
|
2
|
+
|
|
3
|
+
from shadowfinder.shadowfinder import ShadowFinder
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def _validate_args(
|
|
7
|
+
object_height: float,
|
|
8
|
+
shadow_length: float,
|
|
9
|
+
date_time: datetime,
|
|
10
|
+
) -> None:
|
|
11
|
+
"""
|
|
12
|
+
Validate the text search CLI arguments, raises an error if the arguments are invalid.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
if not object_height:
|
|
16
|
+
raise ValueError("Object height cannot be empty")
|
|
17
|
+
if not shadow_length:
|
|
18
|
+
raise ValueError("Shadow length cannot be empty")
|
|
19
|
+
if not date_time:
|
|
20
|
+
raise ValueError("Date time cannot be empty")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ShadowFinderCli:
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def find(
|
|
27
|
+
object_height: float,
|
|
28
|
+
shadow_length: float,
|
|
29
|
+
date: str,
|
|
30
|
+
time: str,
|
|
31
|
+
) -> None:
|
|
32
|
+
"""
|
|
33
|
+
Find the shadow length of an object given its height and the date and time.
|
|
34
|
+
:param object_height: Height of the object in arbitrary units
|
|
35
|
+
:param shadow_length: Length of the shadow in arbitrary units
|
|
36
|
+
:param date: Date in the format YYYY-MM-DD
|
|
37
|
+
:param time: UTC Time in the format HH:MM:SS
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
date_time = datetime.strptime(
|
|
42
|
+
f"{date} {time}", "%Y-%m-%d %H:%M:%S"
|
|
43
|
+
).replace(tzinfo=timezone.utc)
|
|
44
|
+
except Exception as e:
|
|
45
|
+
raise ValueError(f"Invalid argument type or format: {e}")
|
|
46
|
+
_validate_args(object_height, shadow_length, date_time)
|
|
47
|
+
|
|
48
|
+
shadow_finder = ShadowFinder(object_height, shadow_length, date_time)
|
|
49
|
+
shadow_finder.quick_find()
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
from types import SimpleNamespace
|
|
2
|
+
from suncalc import get_position
|
|
3
|
+
import numpy as np
|
|
4
|
+
import matplotlib.pyplot as plt
|
|
5
|
+
import matplotlib.colors as colors
|
|
6
|
+
from mpl_toolkits.basemap import Basemap
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ShadowFinder:
|
|
10
|
+
def __init__(self, object_height, shadow_length, date_time):
|
|
11
|
+
self.object_height = object_height
|
|
12
|
+
self.shadow_length = shadow_length
|
|
13
|
+
self.date_time = date_time
|
|
14
|
+
|
|
15
|
+
self.grid = None
|
|
16
|
+
self.shadow_lengths = None
|
|
17
|
+
|
|
18
|
+
self.fig = None
|
|
19
|
+
|
|
20
|
+
def quick_find(self):
|
|
21
|
+
self.generate_lat_lon_grid()
|
|
22
|
+
self.find_shadows()
|
|
23
|
+
fig = self.plot_shadows()
|
|
24
|
+
fig.savefig(
|
|
25
|
+
f"shadow_finder_{self.date_time.strftime('%Y%m%d-%H%M%S-%Z')}_{self.object_height}_{self.shadow_length}.png"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
def generate_lat_lon_grid(self, angular_resolution=0.25):
|
|
29
|
+
lats = np.arange(-90, 90, angular_resolution)
|
|
30
|
+
lons = np.arange(-180, 180, angular_resolution)
|
|
31
|
+
|
|
32
|
+
lons, lats = np.meshgrid(lons, lats)
|
|
33
|
+
|
|
34
|
+
self.grid = SimpleNamespace(lons=lons, lats=lats)
|
|
35
|
+
|
|
36
|
+
def find_shadows(self):
|
|
37
|
+
# Evaluate the sun's length at a grid of points on the Earth's surface
|
|
38
|
+
|
|
39
|
+
if self.grid is None:
|
|
40
|
+
self.generate_lat_lon_grid()
|
|
41
|
+
|
|
42
|
+
pos_obj = get_position(self.date_time, self.grid.lons, self.grid.lats)
|
|
43
|
+
sun_altitudes = pos_obj["altitude"] # in radians
|
|
44
|
+
|
|
45
|
+
# Calculate the shadow length
|
|
46
|
+
shadow_lengths = self.object_height / np.apply_along_axis(
|
|
47
|
+
np.tan, 0, sun_altitudes
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Replace points where the sun is below the horizon with nan
|
|
51
|
+
shadow_lengths[sun_altitudes <= 0] = np.nan
|
|
52
|
+
|
|
53
|
+
# Show the relative difference between the calculated shadow length and the observed shadow length
|
|
54
|
+
shadow_relative_length_difference = (
|
|
55
|
+
shadow_lengths - self.shadow_length
|
|
56
|
+
) / self.shadow_length
|
|
57
|
+
|
|
58
|
+
self.shadow_lengths = shadow_relative_length_difference
|
|
59
|
+
|
|
60
|
+
def plot_shadows(
|
|
61
|
+
self,
|
|
62
|
+
figure_args={"figsize": (12, 6)},
|
|
63
|
+
basemap_args={"projection": "cyl", "resolution": "c"},
|
|
64
|
+
):
|
|
65
|
+
|
|
66
|
+
fig = plt.figure(**figure_args)
|
|
67
|
+
|
|
68
|
+
# Add a simple map of the Earth
|
|
69
|
+
m = Basemap(**basemap_args)
|
|
70
|
+
m.drawcoastlines()
|
|
71
|
+
m.drawcountries()
|
|
72
|
+
|
|
73
|
+
# Deal with the map projection
|
|
74
|
+
x, y = m(self.grid.lons, self.grid.lats)
|
|
75
|
+
|
|
76
|
+
# Set the a color scale and only show the values between 0 and 0.2
|
|
77
|
+
cmap = plt.cm.get_cmap("inferno_r")
|
|
78
|
+
norm = colors.BoundaryNorm(np.arange(0, 0.2, 0.02), cmap.N)
|
|
79
|
+
|
|
80
|
+
# Plot the data
|
|
81
|
+
m.pcolormesh(x, y, np.abs(self.shadow_lengths), cmap=cmap, norm=norm, alpha=0.7)
|
|
82
|
+
|
|
83
|
+
# plt.colorbar(label='Relative Shadow Length Difference')
|
|
84
|
+
plt.title(
|
|
85
|
+
f"Possible Locations at {self.date_time.strftime('%Y-%m-%d %H:%M:%S %Z')}\n(object height: {self.object_height}, shadow length: {self.shadow_length})"
|
|
86
|
+
)
|
|
87
|
+
self.fig = fig
|
|
88
|
+
return fig
|