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.
@@ -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,10 @@
1
+ from shadowfinder.cli import ShadowFinderCli
2
+ import fire
3
+
4
+
5
+ def main_entrypoint():
6
+ fire.Fire(ShadowFinderCli)
7
+
8
+
9
+ if __name__ == "__main__":
10
+ main_entrypoint()
@@ -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