├── .gitignore ├── LICENSE ├── README.md ├── docs ├── img │ ├── ArcGISEarth_2025-03-example.jpg │ ├── ArcGISEarth_2025-03-input.jpg │ ├── EXCEL_2025-02.png │ ├── Exploring-Family-Trees.png │ ├── Google_earth_2025-03.png │ ├── MyHeritage-2023-10-09.png │ ├── WSL-2023-03-31.png │ ├── WSL-2023-04-01-bash.png │ ├── Windows+wsl.png │ ├── gedcomVisual-2023-10-09.png │ ├── markers-2025-03.png │ ├── msedge_2022-01-02_12-36-33.png │ ├── msedge_2022-01-03_16-06-09.png │ ├── msedge_2022-02-02_22-33-16.gif │ ├── msedge_output.kml.png │ ├── msrdc_2025-03-21_20-59-49.png │ ├── pres2020-2.png │ ├── pres2020.png │ ├── python_2025-03-29.png │ └── python_2025-03.png ├── input.png ├── otherlearnings.md ├── output.html ├── output.png ├── running-on-wsl.md ├── shakespeare.html └── tips-ideas.md ├── gedcom-to-map.sln ├── gedcom-to-map ├── __init__.py ├── const.py ├── gedcom-to-map.py ├── gedcom │ ├── GedcomParser.py │ ├── addressref.py │ └── gpslookup.py ├── gedcomDialogs.py ├── gedcomVisualGUI.py ├── gedcomoptions.py ├── gedcomvisual.py ├── gv.py ├── models │ ├── Color.py │ ├── Creator.py │ ├── Human.py │ ├── Line.py │ ├── Pos.py │ └── Rainbow.py └── render │ ├── KmlExporter.py │ ├── Referenced.py │ ├── foliumExp.py │ └── legend.png ├── img └── legend.psd ├── requirements.txt ├── samples ├── antroute.py ├── bronte.ged ├── bronte.html ├── bronte.kml ├── geodat-address-cache.csv ├── input.ged ├── input.kml ├── output.kml ├── pres2020-2.kml ├── pres2020.ged ├── royal92.ged ├── sample-bourbon │ └── bourbon.ged ├── sample-kennedy │ ├── kennedy.ged │ └── photos │ │ ├── jacqueline-bouvier.jpg │ │ └── john-f.-kennedy-jr.jpg ├── shakespeare.ged ├── shakespeare.html └── shakespeare.kml └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | # My stuff 2 | geodat-address-cache*.csv 3 | myTree.html 4 | myTree.kml 5 | .vs/ 6 | other/ 7 | geodat-address-cache-1.csv 8 | geodat-address-cache-2.csv 9 | *.user 10 | 11 | 12 | # Byte-compiled / optimized / DLL files 13 | __pycache__/ 14 | *.py[cod] 15 | *$py.class 16 | 17 | # C extensions 18 | *.so 19 | 20 | # Distribution / packaging 21 | .Python 22 | build/ 23 | develop-eggs/ 24 | dist/ 25 | downloads/ 26 | eggs/ 27 | .eggs/ 28 | lib/ 29 | lib64/ 30 | parts/ 31 | sdist/ 32 | var/ 33 | wheels/ 34 | pip-wheel-metadata/ 35 | share/python-wheels/ 36 | *.egg-info/ 37 | .installed.cfg 38 | *.egg 39 | MANIFEST 40 | 41 | # PyInstaller 42 | # Usually these files are written by a python script from a template 43 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 44 | *.manifest 45 | *.spec 46 | 47 | # Installer logs 48 | pip-log.txt 49 | pip-delete-this-directory.txt 50 | 51 | # Unit test / coverage reports 52 | htmlcov/ 53 | .tox/ 54 | .nox/ 55 | .coverage 56 | .coverage.* 57 | .cache 58 | nosetests.xml 59 | coverage.xml 60 | *.cover 61 | *.py,cover 62 | .hypothesis/ 63 | .pytest_cache/ 64 | 65 | # Translations 66 | *.mo 67 | *.pot 68 | 69 | # Django stuff: 70 | *.log 71 | local_settings.py 72 | db.sqlite3 73 | db.sqlite3-journal 74 | 75 | # Flask stuff: 76 | instance/ 77 | .webassets-cache 78 | 79 | # Scrapy stuff: 80 | .scrapy 81 | 82 | # Sphinx documentation 83 | docs/_build/ 84 | 85 | # PyBuilder 86 | target/ 87 | 88 | # Jupyter Notebook 89 | .ipynb_checkpoints 90 | 91 | # IPython 92 | profile_default/ 93 | ipython_config.py 94 | 95 | # pyenv 96 | .python-version 97 | 98 | # pipenv 99 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 100 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 101 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 102 | # install all needed dependencies. 103 | #Pipfile.lock 104 | 105 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 106 | __pypackages__/ 107 | 108 | # Celery stuff 109 | celerybeat-schedule 110 | celerybeat.pid 111 | 112 | # SageMath parsed files 113 | *.sage.py 114 | 115 | # Environments 116 | .env 117 | .venv 118 | env/ 119 | venv/ 120 | ENV/ 121 | env.bak/ 122 | venv.bak/ 123 | 124 | # Spyder project settings 125 | .spyderproject 126 | .spyproject 127 | 128 | # Rope project settings 129 | .ropeproject 130 | 131 | # mkdocs documentation 132 | /site 133 | 134 | # mypy 135 | .mypy_cache/ 136 | .dmypy.json 137 | dmypy.json 138 | 139 | # Pyre type checker 140 | .pyre/ 141 | /gedcom-to-map/webdrive.py 142 | /samples/sample-bourbon 143 | /samples/sample-kennedy 144 | /samples/shakespeare.html 145 | /Tree.html 146 | /Tree.kml 147 | *.pyperf 148 | /env11 149 | /env8 150 | /env1 151 | /Scripts 152 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 D Jeffrey 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![GitHub Activity][releases-shield]][releases] 2 | [![License][license-shield]]([license]) 3 | ![Project Maintenance][maintenance-shield] 4 | [![GitHub Activity][commits-shield]][commits] 5 | 6 | 7 | # gedcom-to-visualmap 8 | 9 | Read a GEDCOM file and translate the locations into GPS addresses. 10 | The produces different KML map types which should timelines and movements around the earth. 11 | The produces HTML file which is interactive.. 12 | 13 | This contains two interfaces: command-line and GUI (only tested on Windows) 14 | 15 | Orginally forked from [https://github.com/lmallez/gedcom-to-map] 16 | 17 | # How to Run 18 | 19 | Assuming you have Python installed otherwise... https://github.com/PackeTsar/Install-Python#readme 20 | 21 | 1. Clone the repository: 22 | ``` 23 | $ git clone https://github.com/D-Jeffrey/gedcom-to-visualmap 24 | ``` 25 | - or - 26 | 27 | Alternatively download the zip package of the latest [release](https://github.com/D-Jeffrey/gedcom-to-visualmap/releases) and unzip the package into a directory (such as gedcom-to-visualmap) 28 | 29 | 2. Install dependencies: 30 | ``` 31 | $ cd gedcom-to-visualmap 32 | $ pip install -r requirements.txt 33 | ``` 34 | 35 | 3. Run the GUI interface: 36 | ``` 37 | cd gedcom-to-map 38 | python3 gv.py 39 | ``` 40 | 41 | Alternate 3. Start the command line (Not recommended as there are so many options) 42 | ``` 43 | cd gedcom-to-map 44 | python3 gedcom-to-map.py /users/darren/documents/myhertitagetree.ged myTree -main "@I500003@" 45 | ``` 46 | 47 | ## GUI 48 | ![img](docs/img/python_2025-03-29.png) 49 | 50 | To use the GUI version, `File- > Open` and select your .ged file. 51 | Set your options in the GUI interface 52 | - Type in the Output file name (It saves to the same directory as the Load file). 53 | - Result type drives the options mixture 54 | 55 | Once you have selected your options, 56 | - click the `Load` button and it will start to load the GED and then resolve the addresses. 57 | - `Draw Update` button is a 'Save' button. For HTML it will try and open the file with your web browser automatically. For KML, it will save the file so you can load it onto a map. (See below) 58 | - `Open GPS` button will open the CSV file in Excel if you have it... (I'm thinking that does not work on a Mac) Make sure you close it before running again, or it may not be able to update the CSV file. 59 | - `Stop` will allow you to abort the Load/ Resolving of addresses without killing the GUI, allowing you to pick different options. 60 | - Using the `double-left-click` to select the starting person in (Star) 61 | - Use the `right-click` on the list of people to bring up some known details and how it 62 | was geocoded 63 | ![img](docs/img/python_2025-03.png) 64 | 65 | - `Geo Table` open the CSV file for the resolved and cached names. You can edit the fields and change the `alt` column so substatute in a new name for alternate look up. 66 | TODO Needs more description. 67 | - `Trace` Create a list of the individuals from the starting person. See below 68 | - `Browser` Open the web browser using the last current output html file 69 | 70 | - When the people are loaded you can sort by the various columns by clicking on the column. When the list of people is selected for display it is relative to this starting person, unless you select the `Map all people` 71 | - You can resize the window (larger or maximized) to see more details about the people. 72 | - When displaying people on the HTML map, you can choose to list them as 73 | - single people, 74 | - as a group by the last name 75 | - or by their parents 76 | 77 | # 78 | # Built using 79 | | Project | Githib Repo | Documentation | 80 | | --- | --- | --- | 81 | | wxPython | https://github.com/wxWidgets/Phoenix | https://wxpython.org/ 82 | | ged4py | https://github.com/andy-z/ged4py | https://ged4py.readthedocs.io 83 | | simplekml | https://github.com/eisoldt/simplekml | https://app.readthedocs.org/projects/simplekml/ 84 | | geopy | https://github.com/geopy/geopy |https://geopy.readthedocs.io/en/latest/#geocoders | 85 | | folium | https://github.com/python-visualization/folium | https://python-visualization.github.io/folium/latest/| 86 | | xyzservices | https://github.com/geopandas/xyzservices | https://xyzservices.readthedocs.io/en/stable/index.html | 87 | 88 | 89 | # Results 90 | ## KML Example revised 91 | ### Google Earth Online 92 | ![img](docs/img/Google_earth_2025-03.png) 93 | * KML Output : [samples/input.kml](samples/input.kml) using 'native' only 94 | ### ArcGIS Earth 95 | ![img](docs/img/ArcGISEarth_2025-03-input.jpg) 96 | 97 | Go to https://www.google.ca/maps/about/mymaps 98 | - Click on `Getting Started` 99 | - Click `Create a New Map` 100 | - On `Untitled map` click on the `Import` options and open your KML file 101 | #### Note this does not work in Google Earth as the lines don't appear, not sure about other KML viewers. 102 | 103 | The *`geodat-address-cache.csv`* file can be edited to feed back in new Addresses for GeoCoding. Just edit or clear any column except the *Name* column to have it re-lookup that address. Especially useful if you want to make a bad or old-style name resolve to a new name/location. 104 | If you do not have GPS location in your GEDCOM file, then use -born or -born -death so have it use the place where the person was born and/or died. 105 | 106 | * Cache : [samples/geodat-address-cache.csv](samples/geodat-address-cache.csv) 107 | 108 | 109 | 110 | 111 | ![img](docs/img/pres2020.png) 112 | ``` 113 | cd 114 | python3 ..\gedcom-to-map\gedcom-to-map.py pres2020.ged pres2020 -main "@I1@" -format HTML -groupby 1 -nomarkstar -antpath 115 | ``` 116 | 117 | ![img](docs/img/pres2020-2.png) 118 | ``` 119 | python3 ..\gedcom-to-map\gedcom-to-map.py pres2020.ged pres2020-2 -main "@I676@" -format HTML -groupby 1 -nomarkstar -antpath 120 | ``` 121 | 122 | * KML Output : [samples/pres2020-2.kml](samples/pres2020-2.kml) 123 | ``` 124 | python3 ..\gedcom-to-map\gedcom-to-map.py pres2020.ged pres2020-2 -main "@I676@" -format KML 125 | ``` 126 | 127 | ## Trace button 128 | Load your GED file. Make sure that you have set an output file (click on the `Output File` label for quick access to the Save As). Make sure you have selecte HTML mode (not KML). Double click on a person to trace from that person back. Then all the traced individuals will light up as green (with the starting person in grey). Then click on the Trace button. 129 | This will produce a text file and the name will be shown be show in the Information section of the top left. (Same directory as the output but with a different name '.trace.txt. instead of .HTML). If you open this in Excel, you can reformat the last columns and then use that to identify the number of generations. 130 | 131 | ![img](docs/img/EXCEL_2025-02.png) 132 | 133 | ## Cluster Markers 134 | If you turn off the Markers, then it will turn on Clustered markers. Trying that out and seeing if this become a better way to do markers. This is W, working towards leverage this feature more consistantly. 135 | ![img](docs/img/markers-2025-03.png) 136 | 137 | ## Complex Export of MyHeritage - intereactive map 138 | ![img](docs/img/msedge_2022-02-02_22-33-16.gif) 139 | 140 | ## Running on Linux 141 | - [See Running on WSL](docs/running-on-wsl.md) 142 | 143 | ## Other Ideas 144 | - [See Exploring Family trees](docs/otherlearnings.md) 145 | 146 | ## Comparing MyHeritage PedigreeMap Heatmap and GedcomVisual Heatmap 147 | I noticed that the MyHeritage added a heatmap a year or so ago and it has a lot of overlap with the GedcomVisual heatmap. 148 | 149 | ![img](docs/img/MyHeritage-2023-10-09.png) and ![img](docs/img/gedcomVisual-2023-10-09.png) 150 | 151 | 152 | # Output to HTML using folium 153 | 154 | ### Usage 155 | 156 | *Deprecated functionality* 157 | ``` 158 | usage: gedcom-to-map.py [-h] [-main MAIN] [-format {HTML,KML}] [-max_missing MAX_MISSING] [-max_line_weight MAX_LINE_WEIGHT] [-everyone] [-gpscache] [-nogps] [-nomarker] [-nobornmarker] [-noheatmap] 159 | [-maptiletype {1,2,3,4,5,6,7}] [-nomarkstar] [-groupby {0,1,2}] [-antpath] [-heattime] [-heatstep HEATSTEP] [-homemarker] [-born] [-death] 160 | input_file output_file 161 | 162 | convert gedcom to kml file and lookup GPS addresses 163 | 164 | positional arguments: 165 | input_file GEDCOM file, usually ends at .ged 166 | output_file results file, extension will be added if none is given 167 | 168 | optional arguments: 169 | -h, --help show this help message and exit 170 | -main MAIN if this is missing it will use the first person in the GEDCOM file 171 | -format {HTML,KML} type of output result for map format 172 | -max_missing MAX_MISSING 173 | maximum generation missing (0 = no limit) 174 | -max_line_weight MAX_LINE_WEIGHT 175 | Line maximum weight 176 | -everyone Plot everyone in your tree 177 | 178 | Geocoding: 179 | -gpscache Use the GPS cache only 180 | -nogps Do not lookup places using geocode to determine GPS, use built in GPS values 181 | 182 | Folium Map as HTML (format HTML): 183 | -nomarker Turn off the markers 184 | -nobornmarker Turn off the markers for born 185 | -noheatmap Turn off the heat map 186 | -maptiletype {1,2,3,4,5,6,7} 187 | Map tile styles 188 | -nomarkstar Turn off the markers starting person 189 | -groupby {0,1,2} 1 - Family Name, 2 - Person 190 | -antpath Turn on AntPath 191 | -heattime Turn on heatmap timeline 192 | -heatstep HEATSTEP years per heatmap group step 193 | -homemarker Turn on marking homes 194 | 195 | KML processing: 196 | -born use place born for mapping 197 | -death use place born for mapping 198 | ``` 199 | It produces a HTML file which is interactive and shows relationships betwenn childern and parents and where people live 200 | over the years. It includes a heatmap to show busier places. If you zoom in enough, you can see the different markers 201 | which are overlayed on each other. 202 | 203 | 204 | 205 | ``` 206 | cd samples 207 | python3 ..\gedcom-to-map\gedcom-to-map.py input.ged output -format HTML -groupby 1 208 | python3 ..\gedcom-to-map\gedcom-to-map.py input.ged output -main "@I0000@" -format KML 209 | 210 | ``` 211 | 212 | * HTML Output : [docs/output.html](docs/output.html) 213 | 214 | ## TODO 215 | - Add a treed hierarchy selector to enable people as groups and add expand/collapse to navigation 216 | - more troubleshooting on the address lookup 217 | - better way to interact and refine the address resolution hints 218 | - move address hints in a configuration file 219 | - option to remove 'lines' to not core points (such as RESI or other) 220 | - Sort the Last Name by the highest number of people first or by distance from star 221 | - create a marker animation by year (in time steps) 222 | - in Person dialog show something for people still alive (vs None or Unknown) 223 | - add histical timeline and reference events in the area from https://www.vizgr.org/historical-events/ & https://github.com/dh3968mlq/hdtimelines/ 224 | - need to determine how do deal with very large HTML files. Could use a limit of the number of people included in the selection 225 | - Improve the KML version of the maps by grouping and improving the track of a person. Add description bits to people in the KML version 226 | 227 | 228 | ## Issues 229 | - Marriage is not read correctly all the time, limitations with multiple marriages 230 | - Linux does not save FileHistory 231 | 232 | ### GUI 233 | - Need to separate the Load and GPS resolve steps (currently reloads after 30 seconds of looking up values) 234 | 235 | ## Releases 236 | ### v0.2.6.0 237 | - New :main program gv.py 238 | - fixed the logging level settings and reapply of log settings 239 | - bumped folium version 240 | - Added a dynamic Legend to replace the static pixely image 241 | - Large refactored of FoliumExp, including - Tiles selection (move to GUI for selection), Added Cluster Markers, Additional icons for locations types, 242 | ### v0.2.5.5 243 | - Improved Person dialog 244 | - Improved sorting by of GEDCOM ids in the grid 245 | - Linux compatible fixing/testing (fixed SSL Verifcation errors, Geo Table button, get from github) 246 | ### v0.2.5.4 247 | - Busy control is not working 248 | - Refactored GUI classes into new modules gedcomDialogs 249 | - minor bugs: Title layout, selected person coloring 250 | - Fixed grid Sorting... for sure this time 251 | - Fixed kml plotting errors 252 | - Fixed colors on kml and missing alpha 253 | - Added color to kml markers and provide different born, death and native types 254 | ### v0.2.5.3 255 | - Update Colors, and centralize code for color choices 256 | - Person show list of all direct F/M + ancestors + years 257 | - View only direct ancestors 258 | - Sort code re-write 259 | - Better save the state of the options for next use 260 | - Removed icon at the front of people's name in grid 261 | - Multiple marriage is better 262 | - Load tested to 132K GEDCOM records loads in less than 5 minutes, 1600 people loads in 9 seconds. 263 | ### v0.2.5.2 264 | - Improved Person info, now should delta between Active and displayed person 265 | - Finally fixed the Sort issue (mostly). 266 | - Sorts all dates include BC/B.C. 267 | - Added a Find found to search names. 268 | - Provides information about relation to selected person 269 | ### v0.2.5.1 270 | - Worked on KML version of family loop detection 271 | - Added option to open program for KML (or use a http address) 272 | - Better saving of previous options, remembers selection Main person 273 | ### v0.2.5 274 | - Performance increased, feedback, corrected double grid load 275 | - Added detection for family loops 276 | - Fixed Logging options so they work 277 | - Added `Trace` to trace from selected person and dump to a text file 278 | - Added `Save As` Dialog 279 | - Enriched Help About 280 | - Now tracking time 281 | - Estimating the number of addresses to resolve 282 | - Periodic saving of Address resolution (ever 5 minutes or 500 addresses) 283 | ### v0.2.4.2 284 | - Refactoring 285 | ### v0.2.4.1 286 | - Converted it back to Python 3.8 from 3.10 287 | ### v0.2.4 288 | - Major performance increases 289 | - Improved sorting for Last Name, People and None 290 | - Change the HTML layer to deal with license changes to Leaflet map layers 291 | - Added save state to config file 292 | - finally got error messages in color 293 | - improved logging 294 | - fine tune person dialog 295 | - May have fixed busy icon 296 | - Code refactors 297 | - #bug - loading pres2020.ged and sorting on ID, may be other cases 298 | - Improved Statistics Summary 299 | ### v0.2.3.5 300 | - Adjusted Sorting for People list, adjusted adddress and year value for unknown. 301 | - Try and fix the busy icon 302 | - Error catching for parsing input of Gedcom files 303 | - fixed - Uses the correct cache file from the Selected input GED directory 304 | - file selection dialog automatically opens file 305 | ### v0.2.3.4 306 | - Added dynamic highlighting based on main selection for HTML 307 | - Added Statistics menu under Actions 308 | ### v0.2.3.3 309 | - Adjustable GUI Font (See const.py to set it) 310 | ### v0.2.3 311 | - re-org the project files 312 | ### v0.2.2 313 | - corrected imports (removed gedcom package dependacy) and requirements.txt (again) 314 | - on Linux sample 315 | - more pylint 316 | - fixed sorting of people 317 | ### v0.2.1 318 | - Added support for Windows and Mac open of CSV 319 | - more issues with cache, the first time you run it in the new directory 320 | - added Kennedy samples 321 | - improved setup.py 322 | ### v0.2.0 323 | - fixed requirements.txt 324 | - Add highlighting of people used in the draw 325 | - Major improvements to KML Exporting 326 | - improved feedback loop on loading in Visual 327 | - Fixed issue with selection (broken in 0.1.2), fix issue with caching 328 | - Added Legend (Needs work) 329 | ### v0.1.x 330 | - New details dialog about people, fixed issues with GPS lookup, options 331 | - Folded in GUI bugs from @rajeeshp & @frankbracq 332 | - Adjusted GUI and saving of cache file location, Fixed issue if the cache file 333 | 334 | [license-shield]: https://img.shields.io/github/license/D-Jeffrey/gedcom-to-visualmap.svg?style=for-the-badge 335 | [license]: LICENSE 336 | [commits]: https://github.com/D-Jeffrey/gedcom-to-visualmap/commits 337 | [commits-shield]: https://img.shields.io/github/commit-activity/y/D-Jeffrey/gedcom-to-visualmap?style=for-the-badge 338 | [maintenance-shield]: https://img.shields.io/maintenance/yes/2025.svg?style=for-the-badge 339 | [releases-shield]: https://img.shields.io/github/v/release/D-Jeffrey/gedcom-to-visualmap.svg?style=for-the-badge 340 | [releases]: https://github.com/crowbarz/D-Jeffrey/gedcom-to-visualmap/releases 341 | -------------------------------------------------------------------------------- /docs/img/ArcGISEarth_2025-03-example.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/ArcGISEarth_2025-03-example.jpg -------------------------------------------------------------------------------- /docs/img/ArcGISEarth_2025-03-input.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/ArcGISEarth_2025-03-input.jpg -------------------------------------------------------------------------------- /docs/img/EXCEL_2025-02.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/EXCEL_2025-02.png -------------------------------------------------------------------------------- /docs/img/Exploring-Family-Trees.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/Exploring-Family-Trees.png -------------------------------------------------------------------------------- /docs/img/Google_earth_2025-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/Google_earth_2025-03.png -------------------------------------------------------------------------------- /docs/img/MyHeritage-2023-10-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/MyHeritage-2023-10-09.png -------------------------------------------------------------------------------- /docs/img/WSL-2023-03-31.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/WSL-2023-03-31.png -------------------------------------------------------------------------------- /docs/img/WSL-2023-04-01-bash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/WSL-2023-04-01-bash.png -------------------------------------------------------------------------------- /docs/img/Windows+wsl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/Windows+wsl.png -------------------------------------------------------------------------------- /docs/img/gedcomVisual-2023-10-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/gedcomVisual-2023-10-09.png -------------------------------------------------------------------------------- /docs/img/markers-2025-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/markers-2025-03.png -------------------------------------------------------------------------------- /docs/img/msedge_2022-01-02_12-36-33.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/msedge_2022-01-02_12-36-33.png -------------------------------------------------------------------------------- /docs/img/msedge_2022-01-03_16-06-09.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/msedge_2022-01-03_16-06-09.png -------------------------------------------------------------------------------- /docs/img/msedge_2022-02-02_22-33-16.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/msedge_2022-02-02_22-33-16.gif -------------------------------------------------------------------------------- /docs/img/msedge_output.kml.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/msedge_output.kml.png -------------------------------------------------------------------------------- /docs/img/msrdc_2025-03-21_20-59-49.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/msrdc_2025-03-21_20-59-49.png -------------------------------------------------------------------------------- /docs/img/pres2020-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/pres2020-2.png -------------------------------------------------------------------------------- /docs/img/pres2020.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/pres2020.png -------------------------------------------------------------------------------- /docs/img/python_2025-03-29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/python_2025-03-29.png -------------------------------------------------------------------------------- /docs/img/python_2025-03.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/img/python_2025-03.png -------------------------------------------------------------------------------- /docs/input.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/input.png -------------------------------------------------------------------------------- /docs/otherlearnings.md: -------------------------------------------------------------------------------- 1 | 2 | See you family tree as a graph branches by loading your gedcom into 3 | 4 | https://learnforeverlearn.com/ancestors/ 5 | ![img](img/Exploring-Family-Trees.png) 6 | 7 | ## Visualizing my ancestry on a map 8 | (Dec 2020) this is an interesting approach 9 | https://yannickbrouwer.medium.com/visualizing-my-ancestry-on-a-map-7af6a2354db0 10 | https://github.com/yannickbrouwer/ancestors-migration-visualization 11 | 12 | [Legacy Family Tree](https://legacyfamilytree.com/) This is great for plugging into Family Search as it is one the few authorized applications allowed to pull history and hierarchy from Family Search. I used it to go back 60 generation (which implies up to 60 ^ 4 = 12,960,000 direct ancestors assuming that no one was ever lost). I ended up with about 132,000 ancestors for myself and my wife which took over 30 hours to gather. 13 | 14 | ## API and Applications for Family Searcch 15 | - https://www.familysearch.org/en/developers/docs/api/resources 16 | - https://www.familysearch.org/en/innovate/solutionsgallery/ 17 | 18 | Different Family Tree software chocies 19 | https://www.amyjohnsoncrow.com/what-kind-of-online-family-tree-is-right-for-you/ 20 | - Online Family Tree 21 | - Individual Family Tree on Someone’s Site 22 | - Individual Family Tree on Your Own Site 23 | - Collaborative Family Trees 24 | 25 | ## Timeslines to overlap with history at the time 26 | https://progenygenealogy.com/downloads/timeline-files/ 27 | 28 | 29 | ## MyHertiage Resarch 30 | https://www.myheritage.com/research/catalog 31 | 32 | ## ERSI Story Maps 33 | Go to https://storymaps.arcgis.com/ 34 | Sign up for an account and then you can create a map and a story such as: https://storymaps.arcgis.com/stories/d022d3fa9877473990d2ccfb7d08a1f3/ 35 | 36 | ### Return back to [README](../README.md) 37 | 38 | -------------------------------------------------------------------------------- /docs/output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/docs/output.png -------------------------------------------------------------------------------- /docs/running-on-wsl.md: -------------------------------------------------------------------------------- 1 | 2 | # Running gedcomVisualGUI on Linux (WSL) 3 | 4 | ### WIP, looking for feedback 5 | 6 | 7 | ## Linux (WSL - WSL version: 2.4.12.0) 8 | 9 | ``` 10 | sudo apt update 11 | sudo apt upgrade 12 | sudo apt install software-properties-common 13 | sudo add-apt-repository 'ppa:deadsnakes/ppa' 14 | sudo apt-get update 15 | sudo apt install python3.10 16 | sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.10 1 17 | sudo update-alternatives --install /usr/bin/python3 python3 /usr/bin/python3.8 2 18 | sudo update-alternatives --config python3 19 | sudo apt install python3-pip 20 | sudo apt install python3.10-venv 21 | sudo apt install python3-venv python3-pip python3-wheel 22 | sudo apt-get install python3.10-dev 23 | #pip install -U six wheel setuptools 24 | pip install --upgrade pip 25 | 26 | ``` 27 | 28 | 29 | This seems to work best... 30 | ``` 31 | #sudo apt install xubuntu-desktop 32 | sudo apt install xfce4 33 | sudo apt-get install libgtk-3-dev 34 | sudo apt-get install python3-wxgtk4.0 python3-wxgtk-webview4.0 python3-wxgtk-media4.0 35 | sudo apt-get install git curl libsdl2-mixer-2.0-0 libsdl2-image-2.0-0 libsdl2-2.0-0 36 | sudo apt-get reinstall ca-certificates 37 | ``` 38 | 39 | Then you can use UXTerm from the Ubuntu menu of your windows Desktop (Slick) 40 | I'm not sure if any of the steps below (Saved for reference) are required or they are covered by the desktop install 41 | 42 | This seems to be the way people are going on Linux... 43 | 44 | ``` 45 | sudo apt-get install python3-venv 46 | 47 | python3 -m venv gedcom-to-visualmap 48 | source gedcom-to-visualmap/bin/activate 49 | ``` 50 | 51 | Do the install of `wxPython` to make sure it gets configured and setup correctly. You may need to install or setup other 52 | modules as above like (libgtk-3-dev). I read that you should *not* every run `pip` with `sudo` 53 | 54 | ``` 55 | pip install wheel 56 | # this attrdict3 is only required for WSL because of issues in the image 57 | pip install -U attrdict3 58 | pip install wxPython 59 | python -m pip install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-18.04 wxPython 60 | pip install git+https://github.com/D-Jeffrey/gedcom-to-visualmap.git 61 | ``` 62 | 63 | I have not figured out how to use `venv` properly yet, so this a work in progres. 64 | 65 | The egg seems to be generally working (thought I don't have a background understanding of eggs). It appears eggs are obsolute. 66 | 67 | 68 | ##Running on Linux (WSL) 69 | Using the steps of download and unzip release 0.2.1 70 | ``` 71 | pip install -U attrdict3 72 | sudo pip3 install -U -f https://extras.wxpython.org/wxPython4/extras/linux/gtk3/ubuntu-16.04 wxPython 73 | 74 | pip install wxPython 75 | cd gedcom-to-visualmap 76 | pip install -r requirements.txt 77 | python3 gedcomVisualGUI.py 78 | ``` 79 | 80 | ![img](img/WSL-2023-04-01-bash.png) 81 | 82 | ![img](img/WSL-2023-03-31.png) 83 | 84 | 85 | Trying an alternate approach of: 86 | 87 | ``` 88 | sudo apt install xubuntu-desktop 89 | 90 | ``` 91 | 92 | Output from a download and run 93 | ``` 94 | dj@DESKTOP-R3PSDBU:~/gv$ tar -xf ../Downloads/gedcom-to-visualmap-0.2.3.1.tar.gz 95 | dj@DESKTOP-R3PSDBU:~/gv$ ls 96 | gedcom-to-visualmap-0.2.3.1 97 | dj@DESKTOP-R3PSDBU:~/gv$ cd gedcom-to-visualmap-0.2.3.1/ 98 | dj@DESKTOP-R3PSDBU:~/gv/gedcom-to-visualmap-0.2.3.1$ ls 99 | LICENSE docs gedcom-to-map.pyproj img samples 100 | README.md gedcom-to-map gedcom-to-map.sln requirements.txt setup.py 101 | dj@DESKTOP-R3PSDBU:~/gv/gedcom-to-visualmap-0.2.3.1$ pip install -r requirements.txt 102 | Requirement already satisfied: ged4py>=0.4.4 in /home/dj/.local/lib/python3.8/site-packages (from -r requirements.txt (line 1)) (0.4.4) 103 | Requirement already satisfied: simplekml>=1.3.6 in /home/dj/.local/lib/python3.8/site-packages (from -r requirements.txt (line 2)) (1.3.6) 104 | Requirement already satisfied: geopy>=2.3.0 in /home/dj/.local/lib/python3.8/site-packages (from -r requirements.txt (line 3)) (2.3.0) 105 | Requirement already satisfied: folium>=0.14.0 in /home/dj/.local/lib/python3.8/site-packages (from -r requirements.txt (line 4)) (0.14.0) 106 | Requirement already satisfied: wxPython>=4.1.0 in /home/dj/.local/lib/python3.8/site-packages (from -r requirements.txt (line 5)) (4.2.0) 107 | Requirement already satisfied: convertdate in /home/dj/.local/lib/python3.8/site-packages (from ged4py>=0.4.4->-r requirements.txt (line 1)) (2.4.0) 108 | Requirement already satisfied: ansel in /home/dj/.local/lib/python3.8/site-packages (from ged4py>=0.4.4->-r requirements.txt (line 1)) (1.0.0) 109 | Requirement already satisfied: geographiclib<3,>=1.52 in /home/dj/.local/lib/python3.8/site-packages (from geopy>=2.3.0->-r requirements.txt (line 3)) (2.0) 110 | Requirement already satisfied: requests in /usr/lib/python3/dist-packages (from folium>=0.14.0->-r requirements.txt (line 4)) (2.22.0) 111 | Requirement already satisfied: numpy in /home/dj/.local/lib/python3.8/site-packages (from folium>=0.14.0->-r requirements.txt (line 4)) (1.24.2) 112 | Requirement already satisfied: jinja2>=2.9 in /usr/lib/python3/dist-packages (from folium>=0.14.0->-r requirements.txt (line 4)) (2.10.1) 113 | Requirement already satisfied: branca>=0.6.0 in /home/dj/.local/lib/python3.8/site-packages (from folium>=0.14.0->-r requirements.txt (line 4)) (0.6.0) 114 | Requirement already satisfied: six in /usr/lib/python3/dist-packages (from wxPython>=4.1.0->-r requirements.txt (line 5)) (1.14.0) 115 | Requirement already satisfied: pillow in /home/dj/.local/lib/python3.8/site-packages (from wxPython>=4.1.0->-r requirements.txt (line 5)) (9.4.0) 116 | Requirement already satisfied: pymeeus<=1,>=0.3.13 in /home/dj/.local/lib/python3.8/site-packages (from convertdate->ged4py>=0.4.4->-r requirements.txt (line 1)) (0.5.12) 117 | dj@DESKTOP-R3PSDBU:~/gv/gedcom-to-visualmap-0.2.3.1$ pwd 118 | /home/dj/gv/gedcom-to-visualmap-0.2.3.1 119 | dj@DESKTOP-R3PSDBU:~/gv/gedcom-to-visualmap-0.2.3.1$ ls 120 | LICENSE docs gedcom-to-map.pyproj img samples 121 | README.md gedcom-to-map gedcom-to-map.sln requirements.txt setup.py 122 | dj@DESKTOP-R3PSDBU:~/gv/gedcom-to-visualmap-0.2.3.1$ python3 gedcom-to-map/gedcomVisualGUI.py 123 | 01-04-2023 06:29:51 : INFO : gedcomVisualGUI : : 1315 : Starting up gedcom-to-visualmap 0.2.3 124 | 01-04-2023 06:29:51 : INFO : gedcomVisualGUI : : 1315 : Starting up gedcom-to-visualmap 0.2.3 125 | 126 | 127 | ``` 128 | 129 | ## Side by side 130 | Windows and Linux (WSL) running on Windows 11 - WSL version: 2.4.12.0 - Ubuntu 131 | ![img](img/Windows+wsl.png) 132 | 133 | 134 | # OLD -- OLD -- Saved for reference 135 | 136 | I'm not sure if these steps are still required 137 | 138 | This is to sets up and and installed X-Windows for WSL using xfce4 using the 139 | guidance from https://askubuntu.com/questions/1252007/opening-ubuntu-20-04-desktop-on-wsl2/1365455#1365455 140 | 141 | ``` 142 | sudo apt install pkg-config 143 | sudo apt install libgtk-3-dev 144 | 145 | sudo apt install xrdp xfce4 146 | # If asked, select lightdm, although it probably doesn't matter 147 | 148 | # Optionally, back up the default config 149 | sudo cp /etc/xrdp/xrdp.ini /etc/xrdp/xrdp.ini.bak 150 | # Windows Pro and higher are often already running RDP on 3389 151 | # Prevent conflicts: 152 | sudo sed -i 's/3389/3390/g' /etc/xrdp/xrdp.ini 153 | 154 | # Prevent Wayland from being used in Xrdp 155 | echo "export WAYLAND_DISPLAY=" > ~/.xsessionrc 156 | 157 | # Optional, if you only have one desktop environment installed 158 | echo startxfce4 > ~/.xsession 159 | sudo service xrdp start 160 | 161 | ``` 162 | Now that you have X installed you can access it by logging into it view a Remote Desktop Connection to `localhost:3390` 163 | 164 | You will be prompted for you WSL username and password. (I login the Xorg as the Session type) 165 | 166 | 167 | ### Return back to [README](../README.md) -------------------------------------------------------------------------------- /docs/shakespeare.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 26 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /docs/tips-ideas.md: -------------------------------------------------------------------------------- 1 | # KML Mode 2 | KML mode has two approaches to generating KML for your GED family. The first is Native which assuming that you have GPS positional values in your GED file. This is done using PLAC with a _LATI and _LONG (or LATI and LONG) attributes 3 | 4 | for example 5 | ``` 6 | 2 PLAC Lyon, France 7 | 3 MAP 8 | 4 LATI N45.757814 9 | 4 LONG E4.832011 10 | ``` 11 | or 12 | ``` 13 | 2 PLAC Our Lady of Calvary Cemetery, 134 Forest Street, Yarmouth, Yarmouth County, Nova Scotia, Canada 14 | 3 MAP 15 | 4 LATI 43.831944 16 | 4 LONG -66.102222 17 | ``` 18 | 19 | # Options 20 | 21 | ## KML Command Line 22 | set the KML to open Google Earth by setting the KML value to: 23 | ``` 24 | https://earth.google.com/ 25 | ``` 26 | 27 | If you are using Esri.ArcGISEarth or Google Earth Pro installed on your computer, you can set the launch command line to be: 28 | ``` 29 | $n 30 | ``` -------------------------------------------------------------------------------- /gedcom-to-map.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.33328.57 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{888888A0-9F3D-457C-B088-3A5042F75D52}") = "gedcom-to-map ", "gedcom-to-map .pyproj", "{588881E5-ACD3-413B-9A45-7DB0BCB4A3CB}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {588881E5-ACD3-413B-9A45-7DB0BCB4A3CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {588881E5-ACD3-413B-9A45-7DB0BCB4A3CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ExtensibilityGlobals) = postSolution 21 | SolutionGuid = {6FD69353-747D-456F-A333-FAA50690829A} 22 | EndGlobalSection 23 | EndGlobal 24 | -------------------------------------------------------------------------------- /gedcom-to-map/__init__.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # from models.Human import Human, LifeEvent 4 | # from models.Pos import Pos 5 | # from models.Line import Line 6 | from models.Color import Color 7 | 8 | # from gedcomoptions import gvOptions 9 | # from gedcomvisual import gedcom_to_map, Geoheatmap 10 | -------------------------------------------------------------------------------- /gedcom-to-map/const.py: -------------------------------------------------------------------------------- 1 | """Constants for gedcom-to-visualmap""" 2 | 3 | VERSION = "0.2.6.0" 4 | NAME = "gedcom-to-visualmap" 5 | GEOCODEUSERAGENT = NAME + "/" + VERSION + " GEDCOM-to-map-folium" 6 | GUINAME = 'GEDCOM Visual Map' 7 | 8 | GV_COUNTRIES_JSON = 'https://raw.githubusercontent.com/nnjeim/world/master/resources/json/countries.json' 9 | GV_STATES_JSON = 'https://raw.githubusercontent.com/nnjeim/world/master/resources/json/states.json' 10 | KMLMAPSURL = "https://www.google.ca/maps/about/mymaps" 11 | ABOUTLINK = "https://github.com/D-Jeffrey/" 12 | 13 | BackgroundProcess = None 14 | panel = None 15 | 16 | 17 | GVFONT = ('Verdana', 8, 11) # General Font family and size (suggested range 8 to 11) and Title 'Visual Mapping Options' size 18 | ABOUTFONT = ('Garamond', 13) # About Font family and size (suggested range 8 to 14) 19 | 20 | LOG_CONFIG = { 21 | 'version': 1, 22 | 'disable_existing_loggers': False, 23 | 24 | # NOTE the following values are supperceded by the values in "AppData\..\Local\gedcomvisual\gedcom-visualmap.ini" 25 | # Clear those values if you want to set loggers values here 26 | 'loggers': { 27 | 'gedcomvisual': { 28 | 'level': 'DEBUG' # Works 29 | }, 30 | } 31 | 32 | } 33 | 34 | 35 | -------------------------------------------------------------------------------- /gedcom-to-map/gedcom-to-map.py: -------------------------------------------------------------------------------- 1 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 2 | # 3 | # 4 | # getcom-to-map : command-line version of interface 5 | # See https://github.com/D-Jeffrey/gedcom-to-visualmap 6 | # 7 | # 8 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 9 | import argparse 10 | import logging 11 | import logging.config 12 | 13 | from const import LOG_CONFIG, NAME, VERSION 14 | from gedcomoptions import gvOptions 15 | from gedcomvisual import Geoheatmap, gedcom_to_map 16 | 17 | _log = logging.getLogger(__name__) 18 | 19 | class ArgParse(argparse.ArgumentParser): 20 | def __init__(self): 21 | super().__init__(description="convert gedcom to kml file and lookup GPS addresses - V" + VERSION ) 22 | self.add_argument('input_file', type=str, help="GEDCOM file, usually ends at .ged") 23 | self.add_argument('output_file', type=str, help="results file, extension will be added if none is given") 24 | self.add_argument('-main', type=str, default=None, help="if this is missing it will use the first person in the GEDCOM file") 25 | self.add_argument('-format', type=str, default='HTML', choices=('HTML', 'KML'), help="type of output result for map format") 26 | self.add_argument('-max_missing', type=int, default=0, help="maximum generation missing (0 = no limit)") 27 | self.add_argument('-max_line_weight', type=int, default=20, help="Line maximum weight") 28 | self.add_argument('-everyone', action='store_true', help="Plot everyone in your tree") 29 | self.add_argument('-log', type=str, default='WARNING', choices=('ERROR', 'WARNING', 'INFO', 'DEBUG'), help="logging level") 30 | self.add_argument('-logfile', type=str, default=None, help="log file name") 31 | 32 | self.geocodegroup = self.add_argument_group('Geocoding') 33 | self.geocodegroup.add_argument('-gpscache', action='store_true', dest='cacheonly', help="Use the GPS cache only") 34 | self.geocodegroup.add_argument('-nogps', action='store_false', dest='usegps', help="Do not lookup places using geocode to determine GPS, use built in GPS values") 35 | 36 | self.htmlgroup = self.add_argument_group('Folium Map as HTML (format HTML)') 37 | self.htmlgroup.add_argument('-nomarker', action='store_true', dest='marksoff', help="Turn off the markers") 38 | self.htmlgroup.add_argument('-nobornmarker', action='store_true', dest='bornmarksoff', help="Turn off the markers for born") 39 | self.htmlgroup.add_argument('-noheatmap', action='store_true', dest='heatmapoff', help="Turn off the heat map") 40 | self.htmlgroup.add_argument('-maptiletype', type=int, default=3, choices=range(1, 8), help="Map tile styles") 41 | self.htmlgroup.add_argument('-nomarkstar', action='store_true', dest='markstaroff', help="Turn off the markers starting person") 42 | self.htmlgroup.add_argument('-groupby', type=int, default=2, choices=range(0,3), help="1 - Family Name, 2 - Person") 43 | self.htmlgroup.add_argument('-antpath', action='store_true', dest='antpath', help="Turn on AntPath") 44 | self.htmlgroup.add_argument('-heattime', action='store_true', dest='heattime', help="Turn on heatmap timeline") 45 | self.htmlgroup.add_argument('-heatstep', type=int, default=5, help="years per heatmap group step") 46 | self.htmlgroup.add_argument('-homemarker', action='store_true', dest='markhomes', help="Turn on marking homes") 47 | 48 | 49 | self.kmlgroup = self.add_argument_group('KML processing') 50 | self.kmlgroup.add_argument('-born', action='store_true', help="use place born for mapping") 51 | self.kmlgroup.add_argument('-death', action='store_true', help="use place born for mapping") 52 | 53 | try: 54 | self.args = self.parse_args() 55 | except Exception as e: 56 | print(repr(e)) 57 | 58 | 59 | 60 | if __name__ == '__main__': 61 | arg_parse = ArgParse() 62 | 63 | 64 | logging.config.dictConfig(LOG_CONFIG) 65 | 66 | _log.setLevel(logging.DEBUG) 67 | _log.info("Starting up %s %s", NAME, VERSION) 68 | 69 | logging.basicConfig(level=logging.INFO) 70 | 71 | myGeoOptions = gvOptions() 72 | myGeoOptions.setmarkers (BornMark = not arg_parse.args.bornmarksoff, MarksOn = not arg_parse.args.marksoff, 73 | HeatMap = not arg_parse.args.heatmapoff, 74 | MapStyle = arg_parse.args.maptiletype, 75 | MarkStarOn = not arg_parse.args.markstaroff, 76 | GroupBy = arg_parse.args.groupby, 77 | AntPath = arg_parse.args.antpath, 78 | HeatMapTimeLine = arg_parse.args.heattime, 79 | HeatMapTimeStep = arg_parse.args.heatstep, 80 | HomeMarker = arg_parse.args.markhomes) 81 | places = {'native':'native'} 82 | if arg_parse.args.born or arg_parse.args.death: 83 | places = {} 84 | if arg_parse.args.born: 85 | places['born'] = 'born' 86 | if arg_parse.args.death: 87 | places['death'] = 'death' 88 | 89 | myGeoOptions.setstatic( arg_parse.args.input_file, arg_parse.args.output_file, 90 | arg_parse.args.format =='HTML', arg_parse.args.main, 91 | arg_parse.args.max_missing, arg_parse.args.max_line_weight, 92 | arg_parse.args.usegps, arg_parse.args.cacheonly, arg_parse.args.everyone) 93 | 94 | if (myGeoOptions.ResultHTML): 95 | Geoheatmap(myGeoOptions) 96 | elif arg_parse.args.format =='KML': 97 | gedcom_to_map(myGeoOptions) 98 | _log.info('Finished') 99 | exit(0) 100 | -------------------------------------------------------------------------------- /gedcom-to-map/gedcom/GedcomParser.py: -------------------------------------------------------------------------------- 1 | __all__ = ['GedcomParser', 'DateFormatter'] 2 | 3 | import logging 4 | import re 5 | from datetime import datetime 6 | from pathlib import Path 7 | from typing import Dict 8 | 9 | from ged4py import GedcomReader 10 | from ged4py.date import DateValueVisitor 11 | from ged4py.model import NameRec, Record 12 | from gedcomoptions import gvOptions 13 | from models.Human import Human, LifeEvent 14 | from models.Pos import Pos 15 | 16 | _log = logging.getLogger(__name__.lower()) 17 | 18 | homelocationtags = ('OCCU', 'CENS', 'EDUC') 19 | otherlocationtags = ('CHR', 'BAPM', 'BASM', 'BAPL', 'IMMI', 'NATU', 'ORDN','ORDI', 'RETI', 20 | 'EVEN', 'CEME', 'CREM' ) 21 | 22 | addrtags = ('ADR1', 'ADR2', 'ADR3', 'CITY', 'STAE', 'POST', 'CTRY') 23 | 24 | thisgvOps = None 25 | 26 | def getgdate (gstr): 27 | r = datetime.fromisocalendar(1000,1,1) 28 | d = m = y = None 29 | if gstr: 30 | k = gstr.value.kind.name 31 | if (k == 'SIMPLE') or (k == 'ABOUT') or (k == 'FROM'): 32 | y = gstr.value.date.year 33 | m = gstr.value.date.month_num 34 | d = gstr.value.date.day 35 | elif (k == 'RANGE') or(k == 'PERIOD'): 36 | y = gstr.value.date1.year 37 | m = gstr.value.date1.month_num 38 | d = gstr.value.date1.day 39 | 40 | elif (k == 'PHRASE'): 41 | #TODO need to fix up 42 | y = y 43 | else: 44 | _log.warning ("Date type; %s", gstr.value.kind.name) 45 | y = (y, 1000) [y == None] 46 | m = (m, 1) [m == None] 47 | d = (d, 1) [d == None] 48 | 49 | r = r.replace(y, m, d) 50 | return r 51 | 52 | def getplace(gedcomtag : Record, placetag ="PLAC"): 53 | 54 | if gedcomtag: 55 | myplace = gedcomtag.sub_tag(placetag) 56 | return myplace.value if myplace else None 57 | 58 | return None 59 | 60 | class GetPosFromTag: 61 | """ build an LifeEvent, but also return the other attributes """ 62 | def __init__(self, gedcomtag : Record, tag : str, placetag ="PLAC"): 63 | self.when = None 64 | self.place = None 65 | self.pos = Pos(None, None) 66 | self.event = None 67 | if tag: 68 | subtag = gedcomtag.sub_tag(tag) 69 | else: 70 | subtag = gedcomtag 71 | if subtag: 72 | self.place = getplace(subtag) 73 | self.when = subtag.sub_tag("DATE") 74 | # Looking for: 75 | # 2 PLAC Our Lady of Calvary Cemetery, 134 Forest Street, Yarmouth, Yarmouth County, Nova Scotia, Canada 76 | # 3 MAP 77 | # 4 LATI 43.831944 78 | # 4 LONG -66.102222 79 | # --or-- 80 | # 4 _LATI 43.831944 81 | # 4 _LONG -66.102222 82 | plactag = subtag.sub_tag(placetag) 83 | if plactag: 84 | maploc = plactag.sub_tag("MAP") 85 | if maploc: 86 | lat = maploc.sub_tag("LATI") 87 | lon = maploc.sub_tag("LONG") 88 | if lat and lon: 89 | self.pos = Pos(lat.value,lon.value) 90 | else: 91 | lat = maploc.sub_tag("_LATI") 92 | lon = maploc.sub_tag("_LONG") 93 | if lat and lon: 94 | self.pos = Pos(lat.value,lon.value) 95 | else: 96 | if hasattr(plactag, 'value') : 97 | # Conderation for : 98 | # 2 PLAC Our Lady of Calvary Cemetery, 134 Forest Street, Yarmouth, Yarmouth County, Nova Scotia, Canada, , , 43.831944,-66.102222 99 | 100 | # Regular expression pattern to match GPS coordinates at the end of the string 101 | pattern = r"(.*?)(?:,\s*(-?\d+\.\d+),\s*(-?\d+\.\d+))?$" 102 | match = re.match(pattern, plactag.value) 103 | 104 | # Match the string using the pattern 105 | if match: 106 | # Extract the main location and optional GPS coordinates 107 | self.place = match.group(1).strip() 108 | lat = match.group(2) 109 | lon = match.group(3) 110 | if lat and lon: 111 | self.pos = Pos(float(lat),float(lon)) 112 | self.event = LifeEvent(self.place, self.when, self.pos, tag) 113 | 114 | 115 | class GedcomParser: 116 | def __init__(self, gOp :gvOptions): 117 | self.file_path = gOp.GEDCOMinput 118 | self.gOp = gOp 119 | gOp.totalGEDpeople = None 120 | gOp.totalGEDfamily = None 121 | global thisgvOps 122 | thisgvOps= gOp 123 | if self.file_path == '': 124 | self.gOp.stopstep("no file to Parse") 125 | else: 126 | fpath = Path(self.file_path) 127 | if fpath.is_file(): 128 | self.gOp.step("GEDCOM Parsing") 129 | else: 130 | self.gOp.stopstep("file does not exist") 131 | _log.warning ("File %s does not exist to read.", self.file_path) 132 | 133 | 134 | self.gOp.parsed = False 135 | 136 | 137 | @staticmethod 138 | def __create_human(record: Record) -> Human: 139 | global thisgvOps 140 | thisgvOps.step() 141 | human = Human(record.xref_id) 142 | human.name = '' 143 | name: NameRec = record.sub_tag("NAME") 144 | if name: 145 | human.first = record.name.first 146 | human.surname =record.name.surname 147 | human.maiden = record.name.maiden 148 | human.name = "{}".format(record.name.format()) 149 | # human.name = "{} {}".format(name.value[0], name.value[1]) 150 | if human.name == '': 151 | human.first = "Unknown" 152 | human.surname = "Unknown" 153 | human.maiden = "Unknown" 154 | 155 | title = record.sub_tag("TITL") 156 | human.title = title.value if title else "" 157 | 158 | # Grab a link to the photo 159 | obj = record.sub_tag("OBJE") 160 | human.photo = None 161 | if (obj): 162 | isjpg = obj.sub_tag("FORM") and obj.sub_tag("FORM").value == 'jpg' 163 | if (isjpg): 164 | human.photo = obj.sub_tag("FILE").value 165 | human.sex = record.sex 166 | # BIRTH TAG 167 | birthtag = GetPosFromTag(record, "BIRT") 168 | human.birth = birthtag.event 169 | human.pos = birthtag.pos 170 | 171 | # Use the Burial Tag as a backup for the Death attributes 172 | # TODO need to code this as backup 173 | burialtag = GetPosFromTag(record, "BURI") 174 | 175 | # DEATH TAG 176 | deathtag = GetPosFromTag(record, "DEAT") 177 | human.death = deathtag.event 178 | 179 | # Last Possible is death (or birth) 180 | if human.death and Pos.hasLocation(human.death.pos): 181 | human.pos = human.death.pos 182 | 183 | homes = {} 184 | allhomes=record.sub_tags("RESI") 185 | if allhomes: 186 | for hom in (allhomes): 187 | alladdr = '' 188 | homadr = hom.sub_tag("ADDR") 189 | if homadr: 190 | for adr in (addrtags): 191 | addrval = homadr.sub_tag(adr) 192 | alladdr = alladdr + " " + addrval.value if addrval else alladdr 193 | # If we don't have an address it is of no use 194 | alladdr = alladdr.strip() 195 | if alladdr != '': 196 | homedate = getgdate(hom.sub_tag("DATE")) 197 | if homedate in homes: 198 | _log.debug ("**Double RESI location for : %s on %s @ %s", human.name, homedate , alladdr) 199 | homes[homedate] = LifeEvent(alladdr, hom.sub_tag("DATE")) 200 | for tags in (homelocationtags): 201 | allhomes=record.sub_tags(tags) 202 | if allhomes: 203 | for hom in (allhomes): 204 | # If we don't have an address it is of no use 205 | plac = getplace(hom) 206 | if plac: 207 | homedate = getgdate(hom.sub_tag("DATE")) 208 | homes[homedate] = LifeEvent(plac, hom.sub_tag("DATE"), what='home') 209 | for tags in (otherlocationtags): 210 | allhomes=record.sub_tags(tags) 211 | if allhomes: 212 | for hom in (allhomes): 213 | # If we don't have an address it is of no use 214 | plac = getplace(hom) 215 | if plac: 216 | otherwhat = tags 217 | otherstype = hom.sub_tag("TYPE") 218 | if otherstype: 219 | otherwhat = otherstype.value 220 | homedate = getgdate(hom.sub_tag("DATE")) 221 | homes[homedate] = LifeEvent(plac, hom.sub_tag("DATE"), what=otherwhat) 222 | 223 | 224 | # Sort them by year 225 | if (homes): 226 | for i in sorted (homes.keys()) : 227 | if human.home: 228 | human.home.append(homes[i]) 229 | else: 230 | human.home = [homes[i]] 231 | 232 | return human 233 | 234 | @staticmethod 235 | def __create_humans(records0) -> Dict[str, Human]: 236 | global thisgvOps 237 | humans = dict() 238 | thisgvOps.step("Reading GED", target=(thisgvOps.totalGEDpeople+thisgvOps.totalGEDfamily)) 239 | for record in records0("INDI"): 240 | if thisgvOps.ShouldStop(): 241 | break 242 | humans[record.xref_id] = GedcomParser.__create_human(record) 243 | familyloop = 0 244 | for record in records0("FAM"): 245 | if thisgvOps.ShouldStop(): 246 | break 247 | familyloop += 1 248 | if familyloop % 15 == 0: 249 | thisgvOps.step(info=f"Family loop {familyloop}", plusStep=15) 250 | husband = record.sub_tag("HUSB") 251 | wife = record.sub_tag("WIFE") 252 | for marry in record.sub_tags("MARR"): 253 | marryevent = GetPosFromTag(marry, None).event 254 | if husband and wife: 255 | if humans[husband.xref_id].marriage: 256 | humans[husband.xref_id].marriage.append((wife.xref_id, marryevent)) 257 | else: 258 | humans[husband.xref_id].marriage = [(wife.xref_id, marryevent)] 259 | if humans[wife.xref_id].marriage : 260 | humans[wife.xref_id].marriage.append((husband.xref_id, marryevent)) 261 | else: 262 | humans[wife.xref_id].marriage = [(husband.xref_id, marryevent)] 263 | if husband: 264 | if humans[husband.xref_id].name == "Unknown": 265 | humans[husband.xref_id].name = "Unknown [Father]" 266 | if wife: 267 | if humans[wife.xref_id].name == "Unknown": 268 | humans[wife.xref_id].name = "Unknown [Mother]" 269 | 270 | for chil in record.sub_tags("CHIL"): 271 | if chil.xref_id not in humans.keys(): 272 | continue 273 | if husband: 274 | humans[chil.xref_id].father = husband.xref_id 275 | 276 | if wife: 277 | humans[chil.xref_id].mother = wife.xref_id 278 | return humans 279 | 280 | def create_humans(self) -> Dict[str, Human]: 281 | global thisgvOps 282 | if self.file_path == '': 283 | return None 284 | fpath = Path(self.file_path) 285 | if not fpath.is_file(): 286 | return None 287 | # TODO Date formating is handled elsewhere so the following in not in effect 288 | # format_visitor = DateFormatter() 289 | 290 | 291 | with GedcomReader(self.file_path) as parser: 292 | thisgvOps.step("Loading GED") 293 | thisgvOps.totalGEDpeople = sum(1 for value in parser.xref0.values() if value[1] == 'INDI') 294 | thisgvOps.totalGEDfamily = sum(1 for value in parser.xref0.values() if value[1] == 'FAM') 295 | return self.__create_humans(parser.records0) 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | class DateFormatter(DateValueVisitor): 304 | """Visitor class that produces string representation of dates. 305 | """ 306 | def visitSimple(self, date): 307 | return f"{date.date}" 308 | def visitPeriod(self, date): 309 | return f"from {date.date1} to {date.date2}" 310 | def visitFrom(self, date): 311 | return f"from {date.date}" 312 | def visitTo(self, date): 313 | return f"to {date.date}" 314 | def visitRange(self, date): 315 | return f"between {date.date1} and {date.date2}" 316 | def visitBefore(self, date): 317 | return f"before {date.date}" 318 | def visitAfter(self, date): 319 | return f"after {date.date}" 320 | def visitAbout(self, date): 321 | return f"about {date.date}" 322 | def visitCalculated(self, date): 323 | return f"calculated {date.date}" 324 | def visitEstimated(self, date): 325 | return f"estimated {date.date}" 326 | def visitInterpreted(self, date): 327 | return f"interpreted {date.date} ({date.phrase})" 328 | def visitPhrase(self, date): 329 | return f"({date.phrase})" 330 | -------------------------------------------------------------------------------- /gedcom-to-map/gedcom/addressref.py: -------------------------------------------------------------------------------- 1 | RemoveWords = {"At her home", "Of,", "(town/ville)", "(town)"} 2 | ReplaceWords = {"Bugsworth" : "Buxworth", 3 | "Nthl" : "Netherlands", 4 | "Alta": "Alberta", 5 | "Sask" : "Saskatchewan", 6 | "Co.": "County", 7 | "Canada West": "Ontario", 8 | "Upper Canada": "Ontario", 9 | "Lower Canada": "Quebec", 10 | "East Canada": "Quebec",} 11 | 12 | PositionalReplacement = {"sk": "Saskatchewan"} 13 | SpecialPatterns = {r"{\d+}, [a-z]+[,\s]+[alberta|manitoba|saskatchewan][,\s]+canada", "Township or Rangeroad"} 14 | class cleanupwords(object): 15 | 16 | # Remove repeated words 17 | 18 | # remove or use words within () 19 | 20 | # strip middle words so you have the location, prov/state, country 21 | 22 | 23 | pass 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /gedcom-to-map/gedcomoptions.py: -------------------------------------------------------------------------------- 1 | __all__ = ['gvOptions'] 2 | from calendar import c 3 | import logging 4 | import os 5 | import platform 6 | import time 7 | 8 | import configparser 9 | from pathlib import Path 10 | from xmlrpc.client import boolean 11 | from wx import LogGeneric 12 | from models.Human import Human, Pos 13 | 14 | 15 | 16 | _log = logging.getLogger(__name__) 17 | 18 | def settings_file_pathname(file_name): 19 | # Get the operating system name 20 | os_name = platform.system() 21 | 22 | # Define the settings file path based on the operating system 23 | if os_name == 'Windows': 24 | settings_file_path = os.path.join(os.getenv('LOCALAPPDATA'), 'gedcomvisual\\') 25 | elif os_name == 'Darwin': 26 | settings_file_path = os.path.join(os.path.expanduser('~'), 'Library', 'Application Support') 27 | elif os_name == 'Linux': 28 | settings_file_path = os.path.join(os.path.expanduser('~'), '.config') 29 | else: 30 | _log.error (f"Unsupported operating system: {os_name}") 31 | return file_name 32 | Path(settings_file_path).mkdir(parents=True, exist_ok=True) 33 | settings_file_path = os.path.join(settings_file_path, file_name) 34 | 35 | _log.debug (f"Settings file location: {settings_file_path}") 36 | return settings_file_path 37 | 38 | AllPlaceType = ['native','born','death'] 39 | 40 | class gvOptions: 41 | def __init__ (self): 42 | 43 | self.gvConfig = None 44 | self.defaults() 45 | self.settingsfile = settings_file_pathname("gedcom-visualmap.ini") 46 | 47 | self.GEDCOMinput = None 48 | self.resultpath = None # directory for .Result 49 | self.Result = '' # output file (could be of resulttype .html or .kml) 50 | self.ResultType = "html" 51 | self.ResultHTML = True 52 | self.Main = None # xref_id 53 | self.mainHuman = None # point to Human 54 | self.Name = None # name of human 55 | self.mainHumanPos = Pos(None, None) 56 | self.MaxMissing = 0 57 | self.MaxLineWeight = 20 58 | self.UseGPS = True 59 | self.CacheOnly = False 60 | self.AllEntities = False 61 | self.PlaceType = {'native':'native'} # Dict add/replace with multiple 'native', 'born' & 'death' 62 | self.GridView = False 63 | 64 | self.showLayerControl = True 65 | self.mapMini = True 66 | self.counter = 0 67 | self.countertarget = 0 68 | self.running = False 69 | self.runningLast = 0 70 | self.runningSince = 0 71 | self.time = time.ctime() 72 | self.parsed = False 73 | self.goodmain = False 74 | self.state = '' 75 | self.gpsfile = None 76 | self.stopping = False 77 | self.lookup = None 78 | self.totalpeople = None 79 | self.stepinfo = '' 80 | self.lastmax = self.counter 81 | self.humans = None 82 | self.Referenced = None 83 | self.panel = None 84 | self.selectedpeople = 0 85 | self.lastlines = None 86 | 87 | os_name = platform.system() 88 | if os_name == 'Windows': 89 | self.KMLcmdline = "notepad $n" 90 | self.CSVcmdline = "$n" 91 | elif os_name == 'Darwin': 92 | self.KMLcmdline = "Numbers $n" 93 | self.CSVcmdline = "libreoffice --calc $n" 94 | elif os_name == 'Linux': 95 | self.KMLcmdline = "nano $n" 96 | self.CSVcmdline = "libreoffice --calc $n" 97 | else: 98 | self.KMLcmdline = "notepad $n" 99 | self.CSVcmdline = "notepad $n" 100 | 101 | self.BackgroundProcess = None # Background Thread for processing set later 102 | self.heritage = None 103 | self.UpdateBackgroundEvent = None 104 | self.totalGEDpeople = None 105 | self.totalGEDfamily = None 106 | self.fileHistory = None 107 | 108 | # Types 0 - boolean, 1: int, 2: str 109 | self.html_keys = {'MarksOn':0, 'HeatMap':0, 'BornMark':0, 'DieMark':0, 'MarkStarOn':0, 'GroupBy':1, 110 | 'UseAntPath':0, 'HeatMapTimeLine':0, 'HeatMapTimeStep':1, 'HomeMarker':0, 'showLayerControl':0, 111 | 'mapMini':0, 'MapStyle':2} 112 | self.core_keys = {'UseGPS':0, 'CacheOnly':0, 'AllEntities':0, 'KMLcmdline':'', 'CSVcmdline':''} 113 | self.logging_keys = ['gedcomvisualgui', 'gedcom.gpslookup', 'ged4py.parser', '__main__', 'gedcomoptions','models.Human','models.Creator'] 114 | 115 | if os.path.exists(self.settingsfile): 116 | self.loadsettings() 117 | 118 | def defaults(self): 119 | 120 | self.MarksOn = True 121 | self.HeatMap = False 122 | self.BornMark = True 123 | self.DieMark = True 124 | 125 | self.MarkStarOn = True 126 | self.GroupBy = 2 127 | self.UseAntPath = False 128 | self.HeatMapTimeLine = False 129 | self.HeatMapTimeStep = 10 130 | self.HomeMarker = False 131 | self.MapStyle = "CartoDB.Voyager" 132 | 133 | def setmarkers (self, MarksOn = True, HeatMap = False, MarkStarOn = True, BornMark = True, DieMark = True, MapStyle = 3, GroupBy=2, UseAntPath=False, HeatMapTimeLine=False, HeatMapTimeStep=1, HomeMarker=False): 134 | 135 | self.MarksOn = MarksOn 136 | self.HeatMap = HeatMap 137 | self.BornMark = BornMark 138 | self.DieMark = DieMark 139 | self.MapStyle = MapStyle 140 | self.MarkStarOn = MarkStarOn 141 | self.GroupBy = GroupBy 142 | self.UseAntPath = UseAntPath 143 | self.HeatMapTimeLine = HeatMapTimeLine 144 | self.HeatMapTimeStep = HeatMapTimeStep 145 | self.HomeMarker = HomeMarker 146 | 147 | 148 | 149 | 150 | 151 | def setstatic(self, GEDCOMinput:2, Result:2, ResultHTML: bool, Main=None, MaxMissing:1 = 0, MaxLineWeight:1 = 20, UseGPS:bool = True, CacheOnly:bool = False, AllEntities:bool = False, PlaceType = {'native':'native'}): 152 | 153 | self.setInput(GEDCOMinput) 154 | self.setResults(Result, ResultHTML) 155 | self.Main = Main 156 | self.Name = None 157 | self.MaxMissing = MaxMissing 158 | self.MaxLineWeight = MaxLineWeight 159 | self.UseGPS = UseGPS 160 | self.CacheOnly = CacheOnly 161 | self.AllEntities = AllEntities # generte output of everyone in the system 162 | self.PlaceType = PlaceType # Dict add/replace with multiple 'native', 'born' & 'death' 163 | 164 | 165 | def loadsettings(self): 166 | self.gvConfig = configparser.ConfigParser() 167 | self.gvConfig.read(self.settingsfile) 168 | 169 | # Ensure all necessary sections exist 170 | for section in ['Core', 'HTML', 'Logging', 'Gedcom.Main', 'KML']: 171 | if section not in self.gvConfig.sections(): 172 | self.gvConfig[section] = {} 173 | 174 | # Load HTML settings 175 | for key, typ in self.html_keys.items(): 176 | value = self.gvConfig['HTML'].get(key, None) 177 | if value is not None: 178 | if typ == 0: # Boolean 179 | setattr(self, key, value.lower() == 'true') 180 | elif typ == 1: # int 181 | setattr(self, key, int(value)) 182 | else: # str 183 | setattr(self, key, value) 184 | 185 | # Load Core settings 186 | for key, typ in self.core_keys.items(): 187 | value = self.gvConfig['Core'].get(key, None) 188 | if value is not None: 189 | if typ == 0: # Boolean 190 | setattr(self, key, value.lower() == 'true') 191 | elif typ == 1: # int 192 | setattr(self, key, int(value)) 193 | else: # str 194 | setattr(self, key, value) 195 | 196 | self.setInput(self.gvConfig['Core'].get('InputFile', ''), generalRequest=False) 197 | self.resultpath, self.Result = os.path.split(self.gvConfig['Core'].get('OutputFile', '')) 198 | self.setResults(self.Result, not ('.kml' in self.Result.lower())) 199 | self.KMLcmdline = self.gvConfig['Core'].get('KMLcmdline', '') 200 | self.PlaceType = [] 201 | for key in AllPlaceType: 202 | if self.gvConfig['KML'][key].lower() == 'true': 203 | self.PlaceType.append(key) 204 | 205 | # Load logging settings 206 | for itm, lvl in self.gvConfig.items('Logging'): 207 | alogger = logging.getLogger(itm) 208 | if alogger: 209 | alogger.setLevel(lvl) 210 | 211 | def savesettings(self): 212 | if not self.gvConfig: 213 | self.gvConfig = configparser.ConfigParser() 214 | for section in ['Core', 'HTML', 'Logging', 'Gedcom.Main', 'KML']: 215 | self.gvConfig[section] = {} 216 | 217 | for key in self.core_keys: 218 | self.gvConfig['Core'][key] = str(getattr(self, key)) 219 | for key in self.html_keys: 220 | self.gvConfig['HTML'][key] = str(getattr(self, key)) 221 | 222 | for key in AllPlaceType: 223 | self.gvConfig['KML'][key] = str(key in self.PlaceType) 224 | 225 | self.gvConfig['Core']['InputFile'] = self.GEDCOMinput 226 | self.gvConfig['Core']['OutputFile'] = os.path.join(self.resultpath, self.Result) 227 | self.gvConfig['Core']['KMLcmdline'] = self.KMLcmdline 228 | if self.GEDCOMinput and self.Main: 229 | name = Path(self.GEDCOMinput).stem 230 | self.gvConfig['Gedcom.Main'][name] = str(self.Main) 231 | #for key in range(0, self.panel.fileConfig.filehistory.GetCount()): 232 | # self.gvConfig['Files'][key] = self.panel.fileConfig[key] 233 | 234 | loggerNames = list(logging.root.manager.loggerDict.keys()) 235 | for logName in loggerNames: 236 | if logging.getLogger(logName).propagate == False: 237 | self.gvConfig['Logging'][logName] = logging.getLevelName(logging.getLogger(logName).getEffectiveLevel()) 238 | with open(self.settingsfile, 'w') as configfile: 239 | self.gvConfig.write(configfile) 240 | 241 | 242 | def setMainHuman(self, mainhuman: Human): 243 | """ Set the name of the starting person """ 244 | newMain = (self.mainHuman != mainhuman and mainhuman and self.Name != mainhuman.name) or mainhuman == None 245 | 246 | self.mainHuman = mainhuman 247 | if mainhuman: 248 | self.Name = mainhuman.name 249 | self.mainHumanPos = mainhuman.bestPos() 250 | else: 251 | self.Name = "" 252 | self.mainHumanPos = None 253 | if newMain: 254 | self.selectedpeople = 0 255 | self.lastlines = None 256 | self.heritage = None 257 | self.Referenced = None 258 | self.GridView = False 259 | 260 | def setMain(self, Main: str): 261 | self.Main = Main 262 | if self.humans and Main in self.humans: 263 | self.setMainHuman(self.humans[Main]) 264 | else: 265 | self.setMainHuman(None) 266 | 267 | def setResults(self, Result, useHTML): 268 | """ Set the Output file and type (Only the file name) """ 269 | self.ResultHTML = useHTML 270 | if (useHTML): 271 | self.ResultType = "html" 272 | else: 273 | self.ResultType = "kml" 274 | self.Result, extension = os.path.splitext(Result) # output file (could be of resulttype .html or .kml) 275 | if self.Result != "": 276 | self.Result = self.Result + "." + self.ResultType 277 | # TODO Update Visual value 278 | 279 | def setInput(self, theInput, generalRequest=True): 280 | """ Set the input file, update output file """ 281 | org = self.GEDCOMinput 282 | # Before we lose track, let's do savesettings (unless we are being called from savesettings) 283 | if self.gvConfig and generalRequest and org: 284 | if org != theInput: 285 | self.savesettings() 286 | self.GEDCOMinput = theInput 287 | if self.gvConfig and self.GEDCOMinput: 288 | name = Path(self.GEDCOMinput).stem 289 | if self.gvConfig['Gedcom.Main'].get(name): 290 | self.setMain(self.gvConfig['Gedcom.Main'].get(name)) 291 | else: 292 | self.setMain(None) 293 | 294 | if self.GEDCOMinput: 295 | filen, extension = os.path.splitext(self.GEDCOMinput) 296 | if extension == "" and self.GEDCOMinput != "": 297 | self.GEDCOMinput = self.GEDCOMinput + ".ged" 298 | #TODO needs refinement 299 | 300 | if org != self.GEDCOMinput: 301 | self.resultpath = os.path.dirname(self.GEDCOMinput) 302 | # Force the output to match the name and location of the input 303 | self.setResults(filen, self.ResultHTML) 304 | else: 305 | self.resultpath = None 306 | if org != self.GEDCOMinput: 307 | self.parsed = False 308 | 309 | def KeepGoing(self): 310 | return not self.ShouldStop() 311 | 312 | def ShouldStop(self): 313 | return self.stopping 314 | 315 | def stopstep(self, state): 316 | """ Update the counter used to show progress to the end user """ 317 | """ return true if we should stop stepping """ 318 | self.state = state 319 | 320 | return True 321 | 322 | def stepCounter(self, newcounter): 323 | """ Update the counter used to show progress to the end user """ 324 | self.counter = newcounter 325 | 326 | def step(self, state = None, info=None, target=-1, resetCounter=True, plusStep=1): 327 | """ Update the counter used to show progress to the end user """ 328 | """ return true if we should stop stepping """ 329 | if state: 330 | self.state = state 331 | if resetCounter: 332 | self.counter = 0 333 | self.running = True 334 | else: 335 | self.counter += plusStep 336 | self.stepinfo = info 337 | if target>-1: 338 | self.countertarget = target 339 | # logging.debug(">>>>>> stepped %d", self.counter) 340 | return self.ShouldStop() 341 | 342 | 343 | def stop(self): 344 | self.running = False 345 | self.stopping = False 346 | time.sleep(.1) 347 | self.lastmax = self.counter 348 | self.time = time.ctime() 349 | self.counter = 0 350 | self.state = "" 351 | self.running = False # Race conditions 352 | self.stopping = False 353 | 354 | def get (self, attribute): 355 | """ check an gOptions attribute """ 356 | return getattr(self,attribute) 357 | 358 | def set(self, attribute, value): 359 | """ set an gOptions attribute """ 360 | if not hasattr(self, attribute): 361 | _log.error(f'attempting to set an attribute : {attribute} which does not exist') 362 | raise ValueError(f'attempting to set an attribute : {attribute} which does not exist') 363 | if attribute == "Main": 364 | self.setMain(value) 365 | else: 366 | setattr (self, attribute, value) 367 | 368 | 369 | -------------------------------------------------------------------------------- /gedcom-to-map/gedcomvisual.py: -------------------------------------------------------------------------------- 1 | __all__ = ['gedcom_to_map', 'Geoheatmap', 'doTrace', 'doTraceTo', 'ParseAndGPS', 'doHTML', 'doKML'] 2 | 3 | import logging 4 | import os.path 5 | import os 6 | import subprocess 7 | import webbrowser 8 | 9 | from gedcom.GedcomParser import GedcomParser 10 | from gedcom.gpslookup import GEDComGPSLookup 11 | from gedcomoptions import gvOptions 12 | from models.Creator import Creator, LifetimeCreator, CreatorTrace, Human 13 | from models.Pos import Pos 14 | from render.foliumExp import foliumExporter 15 | from render.KmlExporter import KmlExporter 16 | from render.Referenced import Referenced 17 | # global BackgroundProcess 18 | from const import BackgroundProcess 19 | 20 | # Thread for controlling the background processes Created in gedcomVisualGUI.py 21 | # BackgroundProcess 22 | 23 | _log = logging.getLogger(__name__) 24 | 25 | def gedcom_to_map(gOp : gvOptions): 26 | 27 | humans = ParseAndGPS(gOp) 28 | doKML(gOp, humans) 29 | 30 | def doKML(gOp : gvOptions, humans: list[Human]): 31 | if (not humans): 32 | return 33 | kmlInstance = None 34 | # Save in case we overwrite 35 | for h in humans.keys(): 36 | humans[h].map = humans[h].pos 37 | 38 | for placeType in gOp.PlaceType: 39 | 40 | if (placeType == 'native'): 41 | for h in humans.keys(): 42 | humans[h].pos = humans[h].map 43 | _log.info ("KML native") 44 | nametag = '' 45 | if (placeType == 'born'): 46 | for h in humans.keys(): 47 | humans[h].pos = Pos(None,None) 48 | if humans[h].birth: 49 | if humans[h].birth.pos: 50 | humans[h].pos = humans[h].birth.pos 51 | _log.info ("KML born") 52 | nametag = ' (b)' 53 | if (placeType == 'death'): 54 | for h in humans.keys(): 55 | humans[h].pos = Pos(None,None) 56 | if humans[h].death: 57 | if humans[h].death.pos: 58 | humans[h].pos = humans[h].death.pos 59 | _log.info ("KML death") 60 | nametag = ' (d)' 61 | 62 | lifeline = Creator(humans, gOp.MaxMissing) 63 | creator = lifeline.create(gOp.Main) 64 | if gOp.AllEntities: 65 | lifeline.createothers(creator) 66 | _log.info ("Total of %i people.", len(creator)) 67 | 68 | if gOp.Main not in humans: 69 | _log.error ("Could not find your starting person: %s", gOp.Main) 70 | gOp.stopstep('Error could not find first person') 71 | return 72 | gOp.setMainHuman(humans [gOp.Main]) 73 | if (not kmlInstance): 74 | kmlInstance = KmlExporter(gOp) 75 | 76 | kmlInstance.export(humans[gOp.Main].pos, creator, nametag, placeType) 77 | kmlInstance.Done() 78 | # TODO restore keys (this is a patch that needs to be changed) 79 | for h in humans.keys(): 80 | humans[h].pos = humans[h].map 81 | if gOp.KMLcmdline: 82 | if gOp.KMLcmdline.startswith('http'): 83 | webbrowser.open(gOp.KMLcmdline, new = 0, autoraise = True) 84 | else: 85 | # TODO 86 | cmdline = gOp.KMLcmdline.replace("$n", os.path.join(gOp.resultpath, gOp.Result)) 87 | _log.info(f"KML Running command line : '{cmdline}'") 88 | gOp.BackgroundProcess.SayInfoMessage(f"KML output to : {os.path.join(gOp.resultpath, gOp.Result)}") 89 | try: 90 | if os.name == 'posix': 91 | subprocess.Popen(["/bin/sh" , cmdline]) 92 | elif os.name == 'nt': 93 | cmd = os.environ.get("SystemRoot") + "\\system32\\cmd.exe" 94 | subprocess.Popen([cmd, "/c", cmdline]) 95 | else: 96 | _log.error(f"Unknwon OS to run command-line: '{cmdline}'") 97 | 98 | except FileNotFoundError: 99 | _log.error(f"command errored: '{cmdline}'") 100 | except Exception as e: 101 | _log.error(f"command errored as: {e} : '{cmdline}'") 102 | 103 | 104 | def Geoheatmap(gOp : gvOptions): 105 | 106 | humans = ParseAndGPS(gOp) 107 | doHTML(gOp, humans, True) 108 | 109 | def doHTML(gOp : gvOptions, humans, fullresult ): 110 | 111 | if (not humans): 112 | return 113 | _log.debug ("Creating Lifeline (fullresult:%s)", fullresult) 114 | lifeline = LifetimeCreator(humans, gOp.MaxMissing) 115 | _log.debug ("Creating Humans ") 116 | creator = lifeline.create(gOp.Main) 117 | if gOp.Main not in humans: 118 | _log.error ("Could not find your starting person: %s", gOp.Main) 119 | gOp.stopstep('Error could not find first person') 120 | return 121 | gOp.setMainHuman(humans[gOp.Main]) 122 | if gOp.AllEntities: 123 | gOp.step('Creating life line for everyone') 124 | lifeline.createothers(creator) 125 | _log.info ("Total of %i people & events.", len(creator)) 126 | gOp.totalpeople = len(creator) 127 | 128 | foliumExporter(gOp).export(humans[gOp.Main], creator, fullresult) 129 | if (fullresult): 130 | webbrowser.open(os.path.join(gOp.resultpath, gOp.Result), new = 0, autoraise = True) 131 | 132 | 133 | 134 | def ParseAndGPS(gOp: gvOptions, stage: int = 0 ): 135 | _log.info ("Starting parsing of GEDCOM : %s (stage: %d)", gOp.GEDCOMinput, stage) 136 | if (stage == 0 or stage == 1): 137 | if gOp.humans: 138 | del gOp.humans 139 | gOp.humans = None 140 | gOp.UpdateBackgroundEvent.updategrid = True 141 | humans = GedcomParser(gOp).create_humans() 142 | gOp.humans = humans 143 | gOp.parsed = True 144 | gOp.goodmain = False 145 | if (stage == 2): 146 | humans = gOp.humans 147 | if (humans and gOp.UseGPS and (stage == 0 or stage == 2)): 148 | _log.info ("Starting Address to GPS resolution") 149 | # TODO This could cause issues 150 | # Check for change in the datetime of CSV 151 | if gOp.lookup: 152 | lookupresults = gOp.lookup 153 | else: 154 | lookupresults = GEDComGPSLookup(humans, gOp) 155 | _log.info ("Completed Geocode") 156 | gOp.lookup = lookupresults 157 | lookupresults.resolve_addresses(humans) 158 | gOp.step('Saving Address Cache') 159 | lookupresults.saveAddressCache() 160 | _log.info ("Completed resolves") 161 | 162 | if humans and (not gOp.Main or not gOp.Main in list(humans.keys())): 163 | gOp.set('Main', list(humans.keys())[0]) 164 | _log.info ("Using starting person: %s (%s)", humans[gOp.Main].name, gOp.Main) 165 | 166 | return humans 167 | 168 | def doTrace(gOp : gvOptions): 169 | 170 | gOp.Referenced = Referenced() 171 | gOp.totalpeople = 0 172 | 173 | if not gOp.humans: 174 | _log.error ("Trace:References no humans.") 175 | return 176 | humans = gOp.humans 177 | if gOp.Main not in humans: 178 | _log.error ("Trace:Could not find your starting person: %s", gOp.Main) 179 | return 180 | gOp.Referenced.add(gOp.Main) 181 | lifeline = CreatorTrace(humans) 182 | #for h in humans.keys(): 183 | creator = lifeline.create(gOp.Main) 184 | 185 | _log.info ("Trace:Total of %i people.", len(creator)) 186 | if creator: 187 | for c in creator: 188 | gOp.Referenced.add(c.human.xref_id,tag=c.path) 189 | 190 | # Trace from the main person to this ID 191 | def doTraceTo(gOp : gvOptions, ToID : Human): 192 | if not gOp.Referenced: 193 | doTrace(gOp) 194 | humans = gOp.humans 195 | heritage = [] 196 | heritage = [("", gOp.mainHuman.name, gOp.mainHuman.refyear()[0], gOp.mainHuman.xref_id)] 197 | if gOp.Referenced.exists(ToID.xref_id): 198 | personRelated = gOp.Referenced.gettag(ToID.xref_id) 199 | humanTrace = gOp.mainHuman 200 | if personRelated: 201 | for r in personRelated: 202 | if r == "0": 203 | humanTrace = humans[humanTrace.father] 204 | tag = "Father" 205 | elif r == "1": 206 | humanTrace = humans[humanTrace.mother] 207 | tag = "Mother" 208 | else: 209 | _log.error("doTrace - neither Father or Mother, how did we get here?") 210 | tag = "?????" 211 | heritage.append((f"[{tag}]",humanTrace.name, humanTrace.refyear()[0], humanTrace.xref_id)) 212 | else: 213 | heritage.append([("NotDirect", ToID.name)]) 214 | gOp.heritage = heritage 215 | return heritage 216 | 217 | -------------------------------------------------------------------------------- /gedcom-to-map/gv.py: -------------------------------------------------------------------------------- 1 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 2 | # 3 | # 4 | # gv.py : main for gedcom-to-map 5 | # See https://github.com/D-Jeffrey/gedcom-to-visualmap 6 | # 7 | # 8 | # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # 9 | #!/usr/bin/env python 10 | 11 | import logging 12 | import logging.config 13 | 14 | import wx 15 | import wx.lib.mixins.inspection as wit 16 | # pylint: disable=no-member 17 | 18 | # Import constants and GUI components 19 | from const import GUINAME, LOG_CONFIG, NAME, VERSION, panel 20 | from gedcomVisualGUI import VisualMapFrame, VisualMapPanel 21 | 22 | # Initialize logger for the application 23 | _log = logging.getLogger(__name__) 24 | 25 | # Define a custom wx.App class with inspection capabilities for debugging 26 | class MyWittedApp(wx.App, wit.InspectionMixin): 27 | def OnInit(self): 28 | # Initialize the inspection tool 29 | self.Init() 30 | return True 31 | 32 | # Main entry point of the program 33 | if __name__ == '__main__': 34 | # WITMODE is a flag for enabling debugging tools. Normal is False 35 | WITMODE = False 36 | 37 | # Configure logging using the specified configuration 38 | logging.config.dictConfig(LOG_CONFIG) 39 | 40 | # Log the startup message with application name and version 41 | _log.info("Starting up %s %s", NAME, VERSION) 42 | 43 | # Create the application instance based on WITMODE 44 | if WITMODE: # Debugging mode 45 | app = MyWittedApp() 46 | else: # Normal mode 47 | app = wx.App() 48 | 49 | # Create the main application frame 50 | visualFrame = VisualMapFrame(None, title=GUINAME, size=(1024, 800), style=wx.DEFAULT_FRAME_STYLE) 51 | 52 | # Create and set up the main panel within the frame 53 | panel = VisualMapPanel(visualFrame) 54 | visualFrame.panel = panel 55 | panel.SetupOptions() # Configure panel options 56 | 57 | 58 | # Show the inspection tool if WITMODE is enabled 59 | if WITMODE: 60 | app.ShowInspectionTool() 61 | 62 | # Display the main frame 63 | visualFrame.Show() 64 | 65 | # Start the application's main event loop 66 | app.MainLoop() 67 | 68 | # Log the shutdown message 69 | _log.info('Finished') 70 | 71 | # Exit the program 72 | exit(0) -------------------------------------------------------------------------------- /gedcom-to-map/models/Color.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Color'] 2 | 3 | class Color: 4 | """ 5 | Initializes a Color object. 6 | 7 | Args: 8 | r (int): The red component of the color (0-255). 9 | g (int): The green component of the color (0-255). 10 | b (int): The blue component of the color (0-255). 11 | a (int): The alpha component of the color (0-255). 12 | 13 | KML does not use "normal" color order (RGB), instead it is in reversed order of Blue, Green, Red, with alpha/opacity in the front, for: 14 | AABBGGRR, where AA is alpha, BB is blue, GG is Green and RR is Red. 15 | https://developers.google.com/kml/documentation/kmlreference#elements-specific-to-colorstyle 16 | 17 | """ 18 | def __init__(self, r=255, g=0, b=0, a=255): 19 | self.r = r 20 | self.g = g 21 | self.b = b 22 | self.a = a 23 | 24 | def to_hexa(self) -> str: 25 | return f"{self.a:02x}{self.b:02x}{self.g:02x}{self.r:02x}" 26 | def to_RGBhex(self) -> str: 27 | return f"{self.r:02x}{self.g:02x}{self.b:02x}" 28 | def __repr__(self): 29 | return f"R{self.r:3}G{self.g:3}B{self.b:3}A{self.a:3}" -------------------------------------------------------------------------------- /gedcom-to-map/models/Creator.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Creator', 'LifetimeCreator', 'DELTA', 'SPACE'] 2 | 3 | import logging 4 | from typing import Dict 5 | 6 | from models.Human import Human, LifeEvent 7 | from models.Line import Line 8 | from models.Pos import Pos 9 | from models.Rainbow import Rainbow 10 | 11 | _log = logging.getLogger(__name__.lower()) 12 | 13 | 14 | SPACE = 2.5 # These values drive how colors are selected 15 | DELTA = 1.5 # These values drive how colors are selected 16 | 17 | def getattrif(obj, attname, attvaluename): 18 | if obj: 19 | if hasattr(obj, attname): 20 | a = getattr(obj, attname) 21 | if hasattr(a, attvaluename): 22 | return getattr(a, attvaluename) 23 | return None 24 | 25 | class Creator: 26 | def __init__(self, humans: Dict[str, Human], max_missing=0): 27 | self.humans = humans 28 | self.rainbow = Rainbow() 29 | self.max_missing = max_missing 30 | self.alltheseids= {} 31 | 32 | def line(self, pos: Pos, current: Human, branch, prof, miss, path="") -> list[Line]: 33 | if current.xref_id in self.alltheseids: 34 | _log.error("Looping Problem: {:2} -LOOP STOP - {} {} -Looping= {:20}".format( prof, self.humans[current.xref_id].name, current.xref_id, path)) 35 | return [] 36 | self.alltheseids[current.xref_id] = current.xref_id 37 | if not current.pos: 38 | return ( 39 | [] 40 | if self.max_missing != 0 and miss >= self.max_missing 41 | else self.link(pos, current, branch, prof, miss + 1, path) 42 | ) 43 | color = (branch + DELTA / 2) / (SPACE ** prof) 44 | _log.info("{:8} {:8} {:2} {:.10f} {} {:20}".format(path, branch, prof, color, self.rainbow.get(color).to_hexa(), current.name)) 45 | line = Line(f"{path:8}\t{current.name}", pos, current.pos, self.rainbow.get(color), path, branch,prof, human=current) 46 | return self.link(current.pos, current, branch, prof, 0, path) + [line] 47 | 48 | def link(self, pos: Pos, current: Human, branch=0, prof=0, miss=0, path="") -> list[Line]: 49 | return (self.line(pos, self.humans[current.father], branch*SPACE, prof+1, miss, f"{path}0") if current.father else []) \ 50 | + (self.line(pos, self.humans[current.mother], branch*SPACE+DELTA, prof+1, miss, path + "1") if current.mother else []) 51 | 52 | def create(self, main_id: str): 53 | if main_id not in self.humans.keys(): 54 | _log.error("Could not find your starting person: %s", main_id) 55 | raise IndexError(f"Missing starting person {main_id}") 56 | 57 | current = self.humans[main_id] 58 | return self.link(current.pos, current) 59 | 60 | def createothers(self,listof): 61 | for human in self.humans: 62 | c = [creates.human.xref_id for creates in listof] 63 | if human not in c: 64 | _log.debug("Others: + %s (%s) (%d)", self.humans[human].name, human, len(listof)) 65 | listof.extend(self.line(self.humans[human].pos, self.humans[human], len(listof)/10, 5, 0, path="")) 66 | 67 | class CreatorTrace: 68 | def __init__(self, humans: Dict[str, Human], max_missing=0): 69 | self.humans = humans 70 | self.rainbow = Rainbow() 71 | self.max_missing = max_missing 72 | self.alltheseids= {} 73 | 74 | def line(self, current: Human, branch, prof, path="") -> list[Line]: 75 | if current.xref_id in self.alltheseids: 76 | _log.error("Looping Trace Problem: {:2} -LOOP STOP - {} {} -Tracing= {:20}".format( prof, self.humans[current.xref_id].name, current.xref_id, path)) 77 | return [] 78 | self.alltheseids[current.xref_id] = current.xref_id 79 | 80 | _log.info("{:8} {:8} {:2} {:20}".format(path, branch, prof, current.name)) 81 | line = Line(f"{path:8}\t{current.name}", None, None, None, path, branch,prof, human=current) 82 | return self.link(current, branch, prof, path) + [line] 83 | 84 | def link(self, current: Human, branch=0, prof=0, path="") -> list[Line]: 85 | return (self.line(self.humans[current.father], 0, prof+1, f"{path}0") if current.father else []) \ 86 | + (self.line(self.humans[current.mother], 0, prof+1, path + "1") if current.mother else []) 87 | 88 | def create(self, main_id: str): 89 | if main_id not in self.humans.keys(): 90 | _log.error("Could not find your starting person: %s", main_id) 91 | raise IndexError(f"Missing starting person {main_id}") 92 | 93 | current = self.humans[main_id] 94 | return self.link(current) 95 | 96 | def createothers(self,listof): 97 | for human in self.humans: 98 | c = [creates.human.xref_id for creates in listof] 99 | if human not in c: 100 | _log.debug("Others: + %s (%s) (%d)", self.humans[human].name, human, len(listof)) 101 | listof.extend(self.line(self.humans[human], len(listof)/10, 5, path="")) 102 | 103 | 104 | class LifetimeCreator: 105 | def __init__(self, humans: Dict[str, Human], max_missing=0): 106 | self.humans = humans 107 | self.rainbow = Rainbow() 108 | self.max_missing = max_missing 109 | self.alltheseids= {} 110 | 111 | def selfline(self, current: Human, branch, prof, miss, path="") -> list[Line]: 112 | # We can not draw a line from Birth to death without both ends --- or can we??? 113 | self.alltheseids[current.xref_id] = current.xref_id 114 | color = (branch + DELTA / 2) / (SPACE ** prof) 115 | if current.birth and current.death: 116 | if current.birth.pos and current.death.pos: 117 | _log.info("{:8} {:8} {:2} {:.10f} {} Self {:20}".format(path, branch, prof, color, self.rainbow.get(color).to_hexa(), current.name)) 118 | else: 119 | _log.info("{:8} {:8} {:2} {:.10f} {} Self {:20}".format(" ", " ", " ", 0, "-SKIP-", current.name)) 120 | midpoints = [] 121 | wyear = None 122 | if current.home: 123 | for h in (range(0,len(current.home))): 124 | if (current.home[h].pos and current.home[h].pos.lat != None): 125 | midpoints.append(LifeEvent(current.home[h].where, current.home[h].whenyear(), current.home[h].pos, current.home[h].what)) 126 | wyear = wyear if wyear else current.home[h].whenyear() 127 | bp = current.birth.pos if current.birth else None 128 | bd = current.death.pos if current.death else None 129 | line = Line(f"{path:8}\t{current.name}", bp, bd, self.rainbow.get(color), path, branch, prof, 'Life', 130 | None, midpoints, current, wyear) 131 | if current.birth: 132 | line.updateWhen(current.birth.whenyear()) 133 | line.updateWhen(wyear) 134 | if current.death: 135 | line.updateWhen(current.death.whenyear()) 136 | return [line] 137 | 138 | # Draw a line from the parents birth to the child birth location 139 | 140 | def line(self, pos: Pos, parent: Human, branch, prof, miss, path="", linestyle="", forhuman: Human = None ) -> list[Line]: 141 | # Check to make sure we are not looping and have been here before 142 | if parent.xref_id in self.alltheseids: 143 | _log.error("Looping Problem: {:2} -LOOP STOP- {} {} -Looping= {:20}".format( prof, parent.name, parent.xref_id, path)) 144 | return [] 145 | if hasattr(parent, 'birth') and parent.birth: 146 | color = (branch + DELTA / 2) / (SPACE ** prof) 147 | _log.info("{:8} {:8} {:2} {:.10f} {} {:20} from {:20}".format(path, branch, prof, color, self.rainbow.get(color).to_hexa(), parent.name, forhuman.name)) 148 | line = Line(f"{path:8}\t{parent.name}", pos, parent.birth.pos, self.rainbow.get(color), path, branch, prof, linestyle, 149 | forhuman, human=parent, when= (parent.birth.whenyear(), getattrif(parent, 'death','whenyear'))) 150 | return self.link(parent.birth.pos, parent, branch, prof, 0, path) + [line] 151 | else: 152 | if self.max_missing != 0 and miss >= self.max_missing: 153 | _log.info("{:8} {:8} {:2} {:.10f} {} Self {:20}".format(" ", " ", " ", 0, "-STOP-", parent.name)) 154 | return [] 155 | return self.link(pos, parent, branch, prof, miss+1, path) 156 | _log.info("{:8} {:8} {:2} {:.10f} {} Self {:20}".format(" ", " ", " ", 0, "-KICK-", parent.name)) 157 | 158 | 159 | def link(self, pos: Pos, current: Human, branch=0, prof=0, miss=0, path="") -> list[Line]: 160 | # Maximun recursion depth. This should never happen 161 | if prof < 480: 162 | return (self.selfline(current, branch*SPACE, prof+1, miss, path)) \ 163 | + (self.line(pos, self.humans[current.father], branch*SPACE, prof+1, miss, path + "F",'father', current) if current.father else []) \ 164 | + (self.line(pos, self.humans[current.mother], branch*SPACE+DELTA, prof+1, miss, path + "M", 'mother', current) if current.mother else []) 165 | else: 166 | _log.warning("{:8} {:8} {:2} {} {} {:20}".format(" ", " ", prof, " ", "-TOO DEEP-", current.name)) 167 | return (self.selfline(current, branch*SPACE, prof+1, miss, path)) + [] + [] 168 | 169 | def create(self, main_id: str): 170 | if main_id not in self.humans.keys(): 171 | _log.error ("Could not find your starting person: %s", main_id) 172 | raise IndexError(f"Missing starting person {main_id}") 173 | current = self.humans[main_id] 174 | 175 | return self.link(current.birth.pos if hasattr(current, 'birth') and current.birth != None else Pos(None, None), current) 176 | 177 | def createothers(self,listof): 178 | for human in self.humans: 179 | c = [creates.human.xref_id for creates in listof] 180 | if human not in c: 181 | _log.debug ("Others: + %s(%s) (%d)", self.humans[human].name, human, len(listof)) 182 | listof.extend(self.selfline(self.humans[human], len(listof)/10, len(listof)/10, 5, path="")) 183 | -------------------------------------------------------------------------------- /gedcom-to-map/models/Human.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Human', 'LifeEvent'] 2 | 3 | import logging 4 | import re 5 | 6 | from models.Pos import Pos 7 | 8 | _log = logging.getLogger(__name__.lower()) 9 | 10 | def DateFromField(field): 11 | if field: 12 | # BC or B.C 13 | if field.lower().find("bc") > 0 or field.lower().find("b.c") > 0: 14 | return -int(field[:field.lower().find("b")]) 15 | if len(field) > 3 and field[3].isdigit(): 16 | return int(field[:4]) 17 | try: 18 | return int(field) 19 | except: 20 | digits = '' 21 | for char in field: 22 | if char.isdigit(): 23 | digits += char 24 | return int(digits) if digits else None 25 | return None 26 | 27 | class Partner: 28 | def __init__(self, xref_id, pos): 29 | self.xref_id = xref_id 30 | self.pos :Pos = pos 31 | def __str__(self): 32 | return f"Human(id={self.xref_id}, Pos={self.pos})" 33 | 34 | class Human: 35 | def __init__(self, xref_id): 36 | self.xref_id = xref_id 37 | self.name = None 38 | self.father : Human = None 39 | self.mother : Human = None 40 | self.pos : Pos = None # save the best postion 41 | self.birth : LifeEvent = None 42 | self.death : LifeEvent = None 43 | # TODO need to deal with multiple mariages 44 | self.marriage = None 45 | # TODO multiple homes 46 | self.home : LifeEvent = None 47 | self.map : Pos = None # used to save the orginal pos values 48 | self.first = None # First Name 49 | self.surname = None # Last Name 50 | self.maiden: None 51 | self.sex: None 52 | self.title = None 53 | 54 | 55 | def __str__(self): 56 | return f"Human(id={self.xref_id}, name={self.name})" 57 | 58 | def __repr__(self): 59 | return f"[ {self.xref_id} : {self.name} - {self.father} & {self.mother} - {self.pos} ]" 60 | 61 | # return "year (Born)" or "year (Died)" or "? (Unknown)" along with year as a string or None 62 | # Example "2010 (Born)", "2010" or "1150 (Died)", "1150" or "? (Unknown)" 63 | def refyear(self): 64 | bestyear = "? (Unknown)" 65 | year = None 66 | if self.birth and self.birth.when: 67 | year = self.birth.whenyear() 68 | bestyear = f"{self.birth.whenyear()} (Born)" if year else bestyear 69 | elif self.death and self.death.when: 70 | year = self.death.whenyear() 71 | bestyear = f"{self.death.whenyear()} (Died)" if year else bestyear 72 | return (bestyear, year) 73 | 74 | def bestlocation(self): 75 | # TODO Best Location should consider if in KML mode and what is selected 76 | best = ["Unknown", ""] 77 | if self.birth and self.birth.pos: 78 | best = [ 79 | str(self.birth.pos), 80 | f"{self.birth.where} (Born)" if self.birth.where else "", 81 | ] 82 | elif self.death and self.death.pos: 83 | best = [ 84 | str(self.death.pos), 85 | f"{self.death.where} (Died)" if self.death.where else "", 86 | ] 87 | return best 88 | 89 | def bestPos(self): 90 | # TODO Best Location should consider if in KML mode and what is selected 91 | # If the location is set in the GED, using MAP attribute then that will be the best 92 | best = Pos(None, None) 93 | if self.map and self.map.hasLocation(): 94 | best = self.map 95 | elif self.birth and self.birth.pos and self.birth.pos.hasLocation(): 96 | best = self.birth.pos 97 | elif self.death and self.death.pos and self.death.pos.hasLocation(): 98 | best = self.death.pos 99 | return best 100 | 101 | class LifeEvent: 102 | def __init__(self, place :str, atime, position : Pos = None, what = None): # atime is a Record 103 | self.where = place 104 | self.when = atime 105 | self.pos = position 106 | self.what = what 107 | 108 | def __repr__(self): 109 | return f"[ {self.when} : {self.where} is {self.what}]" 110 | 111 | def asEventstr(self): 112 | if self: 113 | where = f" at {self.getattr('where')}" if self.where else "" 114 | when = f" about {self.getattr('when')}" if self.when else "" 115 | return f"{when}{where}" 116 | else: 117 | return "" 118 | 119 | def whenyear(self, last = False): 120 | if self.when: 121 | if (isinstance(self.when, str)): 122 | return (self.when) 123 | else: 124 | if self.when.value.kind.name == "RANGE" or self.when.value.kind.name == "PERIOD": 125 | if last: 126 | return self.when.value.date1.year_str 127 | else: 128 | return self.when.value.date2.year_str 129 | elif self.when.value.kind.name == "PHRASE": 130 | # TODO poor error checking here Assumes a year is in this date 131 | if re.search(r"[0-9]{4}", self.when.value.phrase): 132 | try: 133 | return re.search(r"[0-9]{4}", self.when.value.phrase)[0] 134 | except Exception: 135 | return None 136 | else: 137 | if hasattr(self.when.value, 'name') : 138 | _log.warning ("'when' year %s as %s", self.when.value.name, self.when.value.phrase) 139 | else: 140 | _log.warning ("unknown 'when' name %s ", self.when.value) 141 | return None 142 | else: 143 | return self.when.value.date.year_str 144 | return None 145 | 146 | def whenyearnum(self, last = False): 147 | """ 148 | Return 0 if None 149 | """ 150 | return DateFromField(self.whenyear(last)) 151 | 152 | def getattr(self, attr): 153 | if attr == 'pos': 154 | return self.pos 155 | elif attr == 'when': 156 | return self.when.value or "" 157 | elif attr == 'where': 158 | return self.where if self.where else "" 159 | elif attr == 'what': 160 | return self.what if self.what else "" 161 | _log.warning("Life Event attr: %s' object has no attribute '%s'", type(self).__name__, attr) 162 | return None 163 | 164 | def __str__(self): 165 | return f"{self.getattr('where')} : {self.getattr('when')} - {self.getattr('pos')} {self.getattr('what')}" 166 | -------------------------------------------------------------------------------- /gedcom-to-map/models/Line.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Line'] 2 | 3 | #TODO need to improved this subclass 4 | from ged4py.date import DateValueVisitor 5 | from models.Color import Color 6 | from models.Human import Human, LifeEvent, DateFromField 7 | from models.Pos import Pos 8 | 9 | 10 | class Line: 11 | """Create a Line defination for a person from their birth to the death which includes midpoints and the names of the mmidpoints 12 | Include the reference to the human, and the name of their parent, and type of line (life, father, mother) along with the year it occured 13 | prof - how far from orginal, midpoint - (LifeEvent) array, 14 | human - reference to themeselves, 15 | """ 16 | def __init__(self, name: str, a: Pos, b: Pos, color: Color, path: str, branch: float, prof: int, style : str = '', parentofhuman : Human = None, 17 | midpoints:list[LifeEvent]=None, human=None, when: int =None, tag:str='', linetype=''): 18 | self.name = name 19 | # TODO we need to use id to avoid problems with duplicate names 20 | # BUG 21 | 22 | self.a = a 23 | self.b = b 24 | self.color = color 25 | 26 | self.path = path 27 | self.branch = branch 28 | self.prof = prof 29 | self.style = style 30 | self.parentofhuman = parentofhuman 31 | self.midpoints : LifeEvent = midpoints 32 | self.human = human 33 | self.tag = tag 34 | # if len(when) > 1: 35 | # (self.when.a, self.when.b) = (self.valueWhen(when[0]), self.valueWhen(when[1])) 36 | 37 | if when and len(when) > 1: 38 | self.when= self.valueWhen(when[0]) 39 | else: 40 | self.when= self.valueWhen(when) 41 | self.linetype = linetype 42 | 43 | 44 | def __repr__(self): 45 | return f"( {self.a}, {self.b} )" 46 | 47 | def valueWhen(self, newwhen): 48 | return DateFromField(newwhen) 49 | 50 | def updateWhen(self, newwhen): 51 | newwhen = self.valueWhen(newwhen) 52 | if newwhen and not self.when: 53 | self.when = newwhen 54 | if self.when and newwhen and newwhen < self.when: 55 | self.when = newwhen 56 | 57 | def __getattr__(self, attr): 58 | if attr == 'parentofhuman' and self.parentofhuman is None: 59 | return '' 60 | raise AttributeError(f"'{type(self).__name__}' object has no attribute '{attr}'") -------------------------------------------------------------------------------- /gedcom-to-map/models/Pos.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Pos'] 2 | 3 | class Pos: 4 | """ Creater a Position value with a of Lat and Lon """ 5 | def __init__(self, lat, lon): 6 | if lat and lat[0].isalpha(): 7 | lat = lat[1:] if lat[0] == 'N' else f'-{lat[1:]}' 8 | if lon and lon[0].isalpha(): 9 | lon = lon[1:] if lon[0] == 'E' else f'-{lon[1:]}' 10 | self.lat = lat 11 | self.lon = lon 12 | 13 | 14 | def hasLocation(self): 15 | """ Does this Position have a actual value """ 16 | return bool(hasattr(self, "lat") and (self.lat != None and self.lon)) 17 | 18 | 19 | def isNone(self): 20 | """ Does this Position have No location value """ 21 | return (not self.hasLocation()) 22 | 23 | def __repr__(self): 24 | return f"[{self.lat},{self.lon}]" 25 | def __str__(self): 26 | return f"({self.lat},{self.lon})" 27 | -------------------------------------------------------------------------------- /gedcom-to-map/models/Rainbow.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Tint', 'Rainbow'] 2 | from models.Color import Color 3 | 4 | 5 | def merge_color(color_a: Color, color_b: Color, coef): 6 | return Color( 7 | color_a.r * (1 - coef) + color_b.r * coef, 8 | color_a.g * (1 - coef) + color_b.g * coef, 9 | color_a.b * (1 - coef) + color_b.b * coef 10 | ) 11 | 12 | 13 | class Tint: 14 | def __init__(self, x, y, mincolor: Color, maxcolor: Color): 15 | self.min = mincolor 16 | self.max = maxcolor 17 | self.x = x 18 | self.y = y 19 | 20 | def isInside(self, x): 21 | return self.x <= x < self.y 22 | 23 | def getColor(self, x): 24 | diff = (x - self.x) / (self.y - self.x) 25 | return merge_color(self.min, self.max, coef=diff) 26 | 27 | 28 | class Rainbow: 29 | def __init__(self): 30 | self.steps = [ 31 | Color(255, 0, 127), 32 | Color(255, 0, 0), 33 | Color(255, 127, 0), 34 | Color(255, 255, 0), 35 | Color(127, 255, 0), 36 | Color(0, 255, 0), 37 | Color(0, 255, 127), 38 | Color(0, 255, 255), 39 | Color(0, 127, 255), 40 | Color(0, 0, 255), 41 | ] 42 | if len(self.steps) == 0: 43 | raise AssertionError("Rainbow did not initialize") 44 | 45 | @staticmethod 46 | def merge_color(color_a: Color, color_b: Color, coef: float): 47 | return Color( 48 | int(color_a.r * (1 - coef) + color_b.r * coef), 49 | int(color_a.g * (1 - coef) + color_b.g * coef), 50 | int(color_a.b * (1 - coef) + color_b.b * coef) 51 | ) 52 | 53 | def get(self, v: float) -> Color: 54 | if v >= 1 or v < 0: 55 | #TODO Need to improve this hack 56 | 57 | # raise 58 | v = 0.9999 59 | len_steps = len(self.steps ) - 1 60 | step = int(v * len_steps) 61 | pos = v % (1 / len_steps) * len_steps 62 | return self.merge_color(self.steps[step], self.steps[step + 1], pos) 63 | -------------------------------------------------------------------------------- /gedcom-to-map/render/KmlExporter.py: -------------------------------------------------------------------------------- 1 | __all__ = ['KmlExporter'] 2 | 3 | import logging 4 | import math 5 | import os.path 6 | import random 7 | 8 | import simplekml as simplekml 9 | from gedcomoptions import gvOptions 10 | from models.Line import Line 11 | from models.Pos import Pos 12 | from render.Referenced import Referenced 13 | 14 | _log = logging.getLogger(__name__.lower()) 15 | 16 | class KmlExporter: 17 | def __init__(self, gOp: gvOptions): 18 | self.file_name = os.path.join(gOp.resultpath, gOp.Result) 19 | self.max_line_weight = gOp.MaxLineWeight 20 | self.kml = None 21 | self.gOp = gOp 22 | self.gOp.Referenced = Referenced() 23 | random.seed() 24 | self.driftOn = True 25 | self.gOp.totalpeople = 0 26 | self.styleA = None 27 | self.styleB = None 28 | 29 | 30 | def driftPos(self, l : Pos): 31 | if not l or not self.driftOn: 32 | return l 33 | return ((float(l.lon)+(random.random() * 0.001) - 0.0005), float(l.lat)+(random.random() * 0.001) - 0.0005) 34 | 35 | def Done(self): 36 | self.gOp.step("Saving KML") 37 | logging.info("Saved as %s", self.file_name) 38 | self.kml.save(self.file_name) 39 | # self.gOp.stop() 40 | # self.kml = None 41 | def export(self, main: Pos, lines: list[Line], ntag ="", mark="native"): 42 | # marker types are : 43 | # diamond, square, circle, blank, stars 44 | # colors are : 45 | # grn, ltblu, pink, blu, purple, red, wht, ylw 46 | if mark == 'death': 47 | marktype = "stars" 48 | elif mark == "born": 49 | marktype = "circle" 50 | else: 51 | marktype = "blank" 52 | colorA = "ylw" 53 | colorB = "ltblu" 54 | if self.kml: 55 | kml = self.kml 56 | styleA = self.styleA 57 | styleB = self.styleB 58 | else: 59 | kml = simplekml.Kml() 60 | self.kml = kml 61 | styleA = simplekml.Style() 62 | # styleA.labelstyle.color = simplekml.Color.blue # Make the text blue 63 | styleA.labelstyle.scale = 1 # Make the text twice as big 64 | styleA.iconstyle.icon.href = f'https://maps.google.com/mapfiles/kml/paddle/{colorA}-{marktype}.png' # https://kml4earth.appspot.com/icons.html 65 | styleB = simplekml.Style() 66 | # styleB.labelstyle.color = simplekml.Color.pink # Make the text pink 67 | styleB.labelstyle.scale = 1 # Make the text twice as big 68 | styleB.iconstyle.icon.href = f'https://maps.google.com/mapfiles/kml/paddle/{colorB}-{marktype}.png' # https://kml4earth.appspot.com/icons.html 69 | self.styleA = styleA 70 | self.styleB = styleB 71 | 72 | if main: 73 | kml.newpoint(name=(self.gOp.Name + ntag),coords=[ (main.lon, main.lat) ]) 74 | self.gOp.totalpeople += 1 75 | else: 76 | _log.error ("No GPS locations to generate a map.") 77 | 78 | 79 | self.gOp.step("Generating KML") 80 | sorted_lines = sorted(lines, key=lambda x: x.prof) 81 | for line in sorted_lines : 82 | self.gOp.step() 83 | names = line.name.split("\t") 84 | linage = names[0] 85 | name = names[len(names)-1] 86 | if (line.a.lon and line.a.lat): 87 | pnt = kml.newpoint(name=name + ntag, description=linage, coords=[self.driftPos(line.a)]) 88 | self.gOp.Referenced.add(line.human.xref_id, 'kml-a') 89 | self.gOp.totalpeople += 1 90 | if line.when: pnt.TimeStamp.when = line.when 91 | pnt.style = styleA 92 | 93 | # pnt.address = where 94 | if (line.b.lon and line.b.lat): 95 | pnt = kml.newpoint(name=name + ntag, description=linage, coords=[self.driftPos(line.b)]) 96 | self.gOp.Referenced.add(line.human.xref_id, 'kml-b') 97 | self.gOp.totalpeople += 1 98 | if line.when: pnt.TimeStamp.when = line.when 99 | pnt.style = styleB 100 | 101 | # pnt.address = where 102 | 103 | if (line.a.lon and line.a.lat and line.b.lon and line.b.lat): 104 | kml_line = kml.newlinestring(name=name, description=linage, coords=[self.driftPos(line.a), self.driftPos(line.b)]) 105 | kml_line.linestyle.color = line.color.to_hexa() 106 | kml_line.linestyle.width = max( 107 | int(self.max_line_weight/math.exp(0.5*line.prof)), 108 | 1 109 | ) 110 | kml_line.extrude = 1 111 | kml_line.tessellate = 1 112 | kml_line.altitudemode = simplekml.AltitudeMode.clamptoground 113 | else: 114 | _log.warning (f"skipping {line.name} ({line.a.lon}, {line.a.lat}) ({line.b.lon}, {line.b.lat})") 115 | self.Done() 116 | 117 | -------------------------------------------------------------------------------- /gedcom-to-map/render/Referenced.py: -------------------------------------------------------------------------------- 1 | __all__ = ['Referenced'] 2 | 3 | ############################################################################################ 4 | # 5 | # This class has an instance variable items which is a dictionary that stores the items added to the list as keys and their count as values. 6 | # The add function first converts the input string to lower case and checks if it already exists in the dictionary. 7 | # If it does, it increments the count of that item. Otherwise, it adds a new entry to the dictionary with a count of 1. 8 | # The exists function checks if an item exists in the dictionary, also in a case-insensitive way. 9 | # The getcount function returns the count of an item in the dictionary, or 0 if it does not exist. 10 | # The new function resets the items dictionary to an empty dictionary, effectively starting the list over 11 | # 12 | # 13 | 14 | class Referenced: 15 | """referenced keeps track of a list of strings """ 16 | 17 | def __init__(self): 18 | self.items = {} 19 | self.types = {} 20 | 21 | def new(self): 22 | self.__init__() 23 | 24 | def add(self, item, locationtype = None, tag = None): 25 | if item in self.items: 26 | self.items[item]["count"] += 1 27 | self.items[item]["tag"] = tag 28 | else: 29 | self.items[item] = {"value": item, "count": 1, "tag": tag} 30 | if locationtype: 31 | if locationtype in self.types: 32 | self.types[locationtype]["count"] += 1 33 | else: 34 | self.types[locationtype] = {"value": locationtype, "count": 1} 35 | 36 | def exists(self, item): 37 | return item in self.items 38 | 39 | def item(self, item): 40 | return self.items[item] 41 | def gettag(self, item): 42 | return self.items[item]['tag'] 43 | def getcount(self, item): 44 | if item in self.items: 45 | return self.items[item]["count"] 46 | else: 47 | return 0 48 | 49 | def __str__(self): 50 | items_str = ", ".join([f"{v['value']} ({v['count']}x)" for v in self.items.values()]) 51 | return f"[{items_str}]" 52 | 53 | def __repr__(self): 54 | return f"Referenced({self.items})" 55 | def __len__(self): 56 | return len(self.items) 57 | 58 | 59 | -------------------------------------------------------------------------------- /gedcom-to-map/render/legend.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/gedcom-to-map/render/legend.png -------------------------------------------------------------------------------- /img/legend.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/img/legend.psd -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | ged4py>=0.4.4 2 | simplekml>=1.3.6 3 | geopy>=2.3.0 4 | folium>=0.16.0 5 | wxPython>=4.1.0 6 | selenium>=4.0.0 7 | xyzservices>=2025.1.0 8 | -------------------------------------------------------------------------------- /samples/antroute.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/samples/antroute.py -------------------------------------------------------------------------------- /samples/bronte.ged: -------------------------------------------------------------------------------- 1 | 0 HEAD 2 | 1 SOUR webtreeprint.com 3 | 2 VERS 1.0 4 | 2 NAME webtreeprint 5 | 1 DATE 25 OCT 2012 6 | 1 FILE bronte.ged 7 | 1 GEDC 8 | 2 VERS 5.5 9 | 2 FORM LINEAGE-LINKED 10 | 1 CHAR UTF-8 11 | 1 SUBM @SUB1@ 12 | 0 @SUB1@ SUBM 13 | 1 NAME webTreePrint 14 | 0 @I0001@ INDI 15 | 1 NAME Patrick /Brontë/ 16 | 2 GIVN Patrick 17 | 2 SURN Brontë 18 | 1 SEX M 19 | 1 BIRT 20 | 2 DATE 17 MAR 1777 21 | 2 PLAC County Down, Ireland 22 | 1 DEAT 23 | 2 DATE 7 JUN 1861 24 | 2 PLAC Haworth, Yorks. 25 | 1 ALIA Brunty 26 | 1 FAMC @F003@ 27 | 1 FAMS @F001@ 28 | 0 @I0002@ INDI 29 | 1 NAME Maria /Branwell/ 30 | 2 GIVN Maria 31 | 2 SURN Branwell 32 | 1 SEX F 33 | 1 BIRT 34 | 2 DATE 15 APR 1783 35 | 2 PLAC Penzance, Cornwall 36 | 1 DEAT 37 | 2 DATE 15 SEP 1821 38 | 1 FAMC @F004@ 39 | 1 FAMS @F001@ 40 | 0 @I0003@ INDI 41 | 1 NAME Maria /Brontë/ 42 | 2 GIVN Maria 43 | 2 SURN Brontë 44 | 1 SEX F 45 | 1 BIRT 46 | 2 DATE 23 APR 1814 47 | 2 PLAC Clough House, High Town 48 | 1 DEAT 49 | 2 DATE 6 MAY 1825 50 | 2 PLAC Howarth 51 | 1 FAMC @F001@ 52 | 0 @I0004@ INDI 53 | 1 NAME Elizabeth /Brontë/ 54 | 2 GIVN Elizabeth 55 | 2 SURN Brontë 56 | 1 SEX F 57 | 1 BIRT 58 | 2 DATE 8 FEB 1815 59 | 1 DEAT 60 | 2 DATE 15 JUN 1825 61 | 2 PLAC Howarth 62 | 1 FAMC @F001@ 63 | 0 @I0005@ INDI 64 | 1 NAME Charlotte /Brontë/ 65 | 2 GIVN Charlotte 66 | 2 SURN Brontë 67 | 1 SEX F 68 | 1 BIRT 69 | 2 DATE 21 APR 1816 70 | 2 PLAC Thornton, Nr. Bradford 71 | 1 DEAT 72 | 2 DATE 31 MAR 1855 73 | 2 PLAC Howarth 74 | 1 FAMC @F001@ 75 | 1 FAMS @F002@ 76 | 0 @I0006@ INDI 77 | 1 NAME Patrick Branwell /Brontë/ 78 | 2 GIVN Patrick Branwell 79 | 2 SURN Brontë 80 | 1 SEX M 81 | 1 BIRT 82 | 2 DATE 26 JUN 1817 83 | 2 PLAC Thornton, Nr. Bradford 84 | 1 DEAT 85 | 2 DATE 24 SEP 1848 86 | 2 PLAC Howarth 87 | 1 FAMC @F001@ 88 | 0 @I0007@ INDI 89 | 1 NAME Emily Jane /Brontë/ 90 | 2 GIVN Emily Jane 91 | 2 SURN Brontë 92 | 1 SEX F 93 | 1 BIRT 94 | 2 DATE 30 JUL 1818 95 | 2 PLAC Thornton, Nr. Bradford 96 | 1 DEAT 97 | 2 DATE 19 DEC 1848 98 | 2 PLAC Howarth 99 | 1 FAMC @F001@ 100 | 0 @I0008@ INDI 101 | 1 NAME Anne /Brontë/ 102 | 2 GIVN Anne 103 | 2 SURN Brontë 104 | 1 SEX F 105 | 1 BIRT 106 | 2 DATE 17 JAN 1820 107 | 2 PLAC Thornton, Nr. Bradford 108 | 1 DEAT 109 | 2 DATE 28 MAY 1849 110 | 2 PLAC Scarborough 111 | 1 FAMC @F001@ 112 | 0 @I0009@ INDI 113 | 1 NAME Arthur Bell /Nicholls/ 114 | 2 GIVN Arthur Bell 115 | 2 SURN Nicholls 116 | 1 SEX M 117 | 1 FAMS @F002@ 118 | 0 @I0010@ INDI 119 | 1 NAME Eleanor /McClory/ 120 | 2 GIVN Eleanor 121 | 2 SURN McClory 122 | 1 SEX F 123 | 1 FAMS @F003@ 124 | 0 @I0011@ INDI 125 | 1 NAME Hugh /Brunty/ 126 | 2 GIVN Hugh 127 | 2 SURN Brunty 128 | 1 SEX M 129 | 1 BIRT 130 | 2 DATE 1755 131 | 1 DEAT 132 | 2 DATE abt 1808 133 | 1 FAMS @F003@ 134 | 0 @I0012@ INDI 135 | 1 NAME Anne /Carne/ 136 | 2 GIVN Anne 137 | 2 SURN Carne 138 | 1 SEX F 139 | 1 BIRT 140 | 2 DATE APR 1744 141 | 1 DEAT 142 | 2 DATE 19 DEC 1809 143 | 1 FAMS @F004@ 144 | 0 @I0013@ INDI 145 | 1 NAME Thomas /Branwell/ 146 | 2 GIVN Thomas 147 | 2 SURN Branwell 148 | 1 SEX M 149 | 1 BIRT 150 | 2 DATE 1746 151 | 1 DEAT 152 | 2 DATE 5 APR 1808 153 | 1 FAMS @F004@ 154 | 0 @I0014@ INDI 155 | 1 NAME Elizabeth /Branwell/ 156 | 2 GIVN Elizabeth 157 | 2 SURN Branwell 158 | 2 NICK Aunt 159 | 1 SEX F 160 | 1 BIRT 161 | 2 DATE 1776 162 | 1 DEAT 163 | 2 DATE 29 OCT 1842 164 | 1 FAMC @F004@ 165 | 0 @F001@ FAM 166 | 1 HUSB @I0001@ 167 | 1 WIFE @I0002@ 168 | 1 MARR 169 | 2 DATE 29 December 1812 170 | 1 CHIL @I0003@ 171 | 1 CHIL @I0004@ 172 | 1 CHIL @I0005@ 173 | 1 CHIL @I0006@ 174 | 1 CHIL @I0007@ 175 | 1 CHIL @I0008@ 176 | 0 @F002@ FAM 177 | 1 HUSB @I0009@ 178 | 1 WIFE @I0005@ 179 | 1 MARR 180 | 2 DATE 29 JUN 1854 181 | 0 @F003@ FAM 182 | 1 HUSB @I0011@ 183 | 1 WIFE @I0010@ 184 | 1 MARR 185 | 2 DATE 1776 186 | 1 CHIL @I0001@ 187 | 0 @F004@ FAM 188 | 1 HUSB @I0013@ 189 | 1 WIFE @I0012@ 190 | 1 MARR 191 | 2 DATE 1768 192 | 1 CHIL @I0002@ 193 | 1 CHIL @I0014@ 194 | 0 TRLR -------------------------------------------------------------------------------- /samples/bronte.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 32 | 39 | 46 | 47 | Anne Brontë (b) 48 | 49 | -1.8540185,53.7907753,0.0 50 | 51 | 52 | 53 | Patrick Brontë (b) 54 | 0 55 | #2 56 | 57 | -1.8542391896417052,53.79073997341946,0.0 58 | 59 | 60 | 61 | Patrick Brontë (b) 62 | 0 63 | #6 64 | 65 | -6.295047725167226,53.33954300883106,0.0 66 | 67 | 68 | 69 | Patrick Brontë 70 | 0 71 | #18 72 | 73 | 1 74 | 1 75 | clampToGround 76 | -1.8544456216367533,53.79043463849358,0.0 -6.295477221441999,53.33940650119587,0.0 77 | 78 | 79 | 80 | Maria Branwell (b) 81 | 1 82 | #2 83 | 84 | -1.8539922974006464,53.79127515249903,0.0 85 | 86 | 87 | 88 | Maria Branwell (b) 89 | 1 90 | #6 91 | 92 | -5.535498573357091,50.11922415935436,0.0 93 | 94 | 95 | 96 | Maria Branwell 97 | 1 98 | #26 99 | 100 | 1 101 | 1 102 | clampToGround 103 | -1.8541017224019203,53.79114019457889,0.0 -5.535008441864236,50.11971664208801,0.0 104 | 105 | 106 | 107 | Hugh Brunty (b) 108 | 00 109 | #2 110 | 111 | -6.2954758141232485,53.33965903938846,0.0 112 | 113 | 114 | 115 | Eleanor McClory (b) 116 | 01 117 | #2 118 | 119 | -6.295039156198166,53.34001404003507,0.0 120 | 121 | 122 | 123 | Thomas Branwell (b) 124 | 10 125 | #2 126 | 127 | -5.534920601842174,50.119942310051506,0.0 128 | 129 | 130 | 131 | Anne Carne (b) 132 | 11 133 | #2 134 | 135 | -5.534808888308232,50.119532223486644,0.0 136 | 137 | 138 | 139 | 140 | -------------------------------------------------------------------------------- /samples/geodat-address-cache.csv: -------------------------------------------------------------------------------- 1 | name,alt,country,type,class,icon,place_id,lat,long,boundry 2 | "'Lyon, France'","'Lyon, M\xe9tropole de Lyon, Circonscription d\xe9partementale du Rh\xf4ne, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, France'",FR,administrative,boundary,https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png,282297473,45.7578137,4.8320114,"['45.7073666', '45.8082628', '4.7718134', '4.8983774']" 3 | 'Saint-Etienne',"'Saint-\xc9tienne, Loire, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 42016, France'",,city,place,https://nominatim.openstreetmap.org/ui/mapicons//poi_place_city.p.20.png,283907210,45.4401467,4.3873058,"['45.2801467', '45.6001467', '4.2273058', '4.5473058']" 4 | 'Annecy',"'Annecy, Haute-Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 74000, France'",,city,place,https://nominatim.openstreetmap.org/ui/mapicons//poi_place_city.p.20.png,283907990,45.8992348,6.1288847,"['45.7392348', '46.0592348', '5.9688847', '6.2888847']" 5 | 'Chamb\xa9\u266dry','Chamb\xa9\u266dry',,,,,,,, 6 | 'Cruseilles',"'Cruseilles, Saint-Julien-en-Genevois, Haute-Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 74350, France'",,village,place,https://nominatim.openstreetmap.org/ui/mapicons//poi_place_village.p.20.png,46399748,46.0336537,6.1088486,"['46.0136537', '46.0536537', '6.0888486', '6.1288486']" 7 | 'Thorens-Gli\xa9\xb7res','Thorens-Gli\xa9\xb7res',,,,,,,, 8 | 'Roche-la-Moli\xa9\xb7re','Roche-la-Moli\xa9\xb7re',,,,,,,, 9 | 'Aix-les-Bains',"'Aix-les-Bains, Chamb\xe9ry, Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 73100, France'",,town,place,https://nominatim.openstreetmap.org/ui/mapicons//poi_place_town.p.20.png,36474045,45.6886292,5.9146362,"['45.6486292', '45.7286292', '5.8746362', '5.9546362']" 10 | 'Grenoble',"'Grenoble, Is\xe8re, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 38027, France'",,city,place,https://nominatim.openstreetmap.org/ui/mapicons//poi_place_city.p.20.png,283996788,45.1875602,5.7357819,"['45.0275602', '45.3475602', '5.5757819', '5.8957819']" 11 | 'Pers-Jussy',"'Pers-Jussy, Saint-Julien-en-Genevois, Haute-Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 74930, France'",,administrative,boundary,https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png,282288819,46.100556,6.261276231103416,"['46.0744735', '46.1266532', '6.2252214', '6.3025974']" 12 | 'Frangy',"'Frangy, Saint-Julien-en-Genevois, Haute-Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 74270, France'",,administrative,boundary,https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png,282338644,46.02321565,5.922368668441535,"['46.0022508', '46.0439914', '5.8959692', '5.9488274']" 13 | 'Aviernoz',"'Aviernoz, Filli\xe8re, Annecy, Haute-Savoie, Auvergne-Rh\xf4ne-Alpes, France m\xe9tropolitaine, 74570, France'",,administrative,boundary,https://nominatim.openstreetmap.org/ui/mapicons//poi_boundary_administrative.p.20.png,282237397,45.9756168,6.229819726831506,"['45.953135', '45.9980676', '6.1984844', '6.2818995']" 14 | -------------------------------------------------------------------------------- /samples/input.ged: -------------------------------------------------------------------------------- 1 | 0 HEAD 2 | 0 @I0000@ INDI 3 | 1 NAME Pierre /MARTIN/ 4 | 2 GIVN Pierre 5 | 2 SURN MARTIN 6 | 1 SEX M 7 | 1 BIRT 8 | 2 PLAC Lyon, France 9 | 3 MAP 10 | 4 LATI N45.757814 11 | 4 LONG E4.832011 12 | 2 ADDR 13 | 3 CITY Lyon, France 14 | 1 FAMC @F0000@ 15 | 2 PEDI birth 16 | 1 CHAN 17 | 2 DATE 13 APR 2020 18 | 3 TIME 23:33:58 19 | 0 @I0001@ INDI 20 | 1 NAME Antoine /MARTIN/ 21 | 2 GIVN Antoine 22 | 2 SURN MARTIN 23 | 1 SEX M 24 | 1 BIRT 25 | 2 PLAC Saint-Etienne 26 | 3 MAP 27 | 4 LATI N45.440147 28 | 4 LONG E4.387306 29 | 2 ADDR 30 | 3 CITY Saint-Etienne 31 | 1 FAMC @F0001@ 32 | 2 PEDI birth 33 | 1 FAMS @F0000@ 34 | 1 CHAN 35 | 2 DATE 13 APR 2020 36 | 3 TIME 23:36:28 37 | 0 @I0002@ INDI 38 | 1 NAME Céline /BERNARD/ 39 | 2 GIVN Céline 40 | 2 SURN BERNARD 41 | 1 SEX F 42 | 1 BIRT 43 | 2 PLAC Annecy 44 | 3 MAP 45 | 4 LATI N45.899235 46 | 4 LONG E6.128885 47 | 1 FAMC @F0002@ 48 | 2 PEDI birth 49 | 1 FAMS @F0000@ 50 | 1 CHAN 51 | 2 DATE 13 APR 2020 52 | 3 TIME 23:38:16 53 | 0 @I0003@ INDI 54 | 1 NAME Gabriel /MARTIN/ 55 | 2 GIVN Gabriel 56 | 2 SURN MARTIN 57 | 1 SEX M 58 | 1 BIRT 59 | 2 PLAC Saint-Etienne 60 | 3 MAP 61 | 4 LATI N45.440147 62 | 4 LONG E4.387306 63 | 2 ADDR 64 | 3 CITY Saint-Etienne 65 | 1 FAMC @F0003@ 66 | 2 PEDI birth 67 | 1 FAMS @F0001@ 68 | 1 CHAN 69 | 2 DATE 13 APR 2020 70 | 3 TIME 23:39:51 71 | 0 @I0004@ INDI 72 | 1 NAME Henriette /MARIE/ 73 | 2 GIVN Henriette 74 | 2 SURN MARIE 75 | 1 SEX F 76 | 1 BIRT 77 | 2 PLAC Chambéry 78 | 3 MAP 79 | 4 LATI N45.566267 80 | 4 LONG E5.920364 81 | 1 FAMC @F0004@ 82 | 2 PEDI birth 83 | 1 FAMS @F0001@ 84 | 1 CHAN 85 | 2 DATE 13 APR 2020 86 | 3 TIME 23:42:09 87 | 0 @I0005@ INDI 88 | 1 NAME Pierre /BERNARD/ 89 | 2 GIVN Pierre 90 | 2 SURN BERNARD 91 | 1 SEX M 92 | 1 BIRT 93 | 2 PLAC Cruseilles 94 | 3 MAP 95 | 4 LATI N46.033654 96 | 4 LONG E6.108849 97 | 1 FAMC @F0005@ 98 | 2 PEDI birth 99 | 1 FAMS @F0002@ 100 | 1 CHAN 101 | 2 DATE 13 APR 2020 102 | 3 TIME 23:43:46 103 | 0 @I0006@ INDI 104 | 1 NAME Jeanne /THOMAS/ 105 | 2 GIVN Jeanne 106 | 2 SURN THOMAS 107 | 1 SEX F 108 | 1 BIRT 109 | 2 PLAC Thorens-Glières 110 | 3 MAP 111 | 4 LATI N45.987359 112 | 4 LONG E6.306224 113 | 1 FAMC @F0006@ 114 | 2 PEDI birth 115 | 1 FAMS @F0002@ 116 | 1 CHAN 117 | 2 DATE 13 APR 2020 118 | 3 TIME 23:45:33 119 | 0 @I0007@ INDI 120 | 1 NAME Jean-Baptiste /MARTIN/ 121 | 2 GIVN Jean-Baptiste 122 | 2 SURN MARTIN 123 | 1 SEX M 124 | 1 BIRT 125 | 2 PLAC Roche-la-Molière 126 | 3 MAP 127 | 4 LATI N45.434899 128 | 4 LONG E4.321262 129 | 1 FAMS @F0003@ 130 | 1 CHAN 131 | 2 DATE 13 APR 2020 132 | 3 TIME 23:39:51 133 | 0 @I0008@ INDI 134 | 1 NAME Camille /FRAMBOISIER/ 135 | 2 GIVN Camille 136 | 2 SURN FRAMBOISIER 137 | 1 SEX F 138 | 1 BIRT 139 | 2 PLAC Saint-Etienne 140 | 3 MAP 141 | 4 LATI N45.440147 142 | 4 LONG E4.387306 143 | 2 ADDR 144 | 3 CITY Saint-Etienne 145 | 1 FAMS @F0003@ 146 | 1 CHAN 147 | 2 DATE 14 APR 2020 148 | 3 TIME 00:12:36 149 | 0 @I0009@ INDI 150 | 1 NAME Jean-Pierre /MARIE/ 151 | 2 GIVN Jean-Pierre 152 | 2 SURN MARIE 153 | 1 SEX M 154 | 1 BIRT 155 | 2 PLAC Aix-les-Bains 156 | 3 MAP 157 | 4 LATI N45.688629 158 | 4 LONG E5.914636 159 | 1 FAMS @F0004@ 160 | 1 CHAN 161 | 2 DATE 13 APR 2020 162 | 3 TIME 23:42:09 163 | 0 @I0010@ INDI 164 | 1 NAME Anne /CORLIC/ 165 | 2 GIVN Anne 166 | 2 SURN CORLIC 167 | 1 SEX F 168 | 1 BIRT 169 | 2 PLAC Grenoble 170 | 3 MAP 171 | 4 LATI N45.18756 172 | 4 LONG E5.735782 173 | 1 FAMS @F0004@ 174 | 1 CHAN 175 | 2 DATE 13 APR 2020 176 | 3 TIME 23:42:09 177 | 0 @I0011@ INDI 178 | 1 NAME Charles /BERNARD/ 179 | 2 GIVN Charles 180 | 2 SURN BERNARD 181 | 1 SEX M 182 | 1 BIRT 183 | 2 PLAC Pers-Jussy 184 | 3 MAP 185 | 4 LATI N46.106677 186 | 4 LONG E6.267664 187 | 1 FAMS @F0005@ 188 | 1 CHAN 189 | 2 DATE 13 APR 2020 190 | 3 TIME 23:43:21 191 | 0 @I0012@ INDI 192 | 1 NAME Annie /VERSAIRE/ 193 | 2 GIVN Annie 194 | 2 SURN VERSAIRE 195 | 1 SEX F 196 | 1 BIRT 197 | 2 PLAC Frangy 198 | 3 MAP 199 | 4 LATI N46.019066 200 | 4 LONG E5.929342 201 | 1 FAMS @F0005@ 202 | 1 CHAN 203 | 2 DATE 13 APR 2020 204 | 3 TIME 23:43:46 205 | 0 @I0013@ INDI 206 | 1 NAME Antonin /THOMAS/ 207 | 2 GIVN Antonin 208 | 2 SURN THOMAS 209 | 1 SEX M 210 | 1 BIRT 211 | 2 PLAC Thorens-Glières 212 | 3 MAP 213 | 4 LATI N45.987359 214 | 4 LONG E6.306224 215 | 1 FAMS @F0006@ 216 | 1 CHAN 217 | 2 DATE 13 APR 2020 218 | 3 TIME 23:44:58 219 | 0 @I0014@ INDI 220 | 1 NAME Bernadette /COULEUVRE/ 221 | 2 GIVN Bernadette 222 | 2 SURN COULEUVRE 223 | 1 SEX F 224 | 1 BIRT 225 | 2 PLAC Aviernoz 226 | 3 MAP 227 | 4 LATI N45.975617 228 | 4 LONG E6.22982 229 | 1 FAMS @F0006@ 230 | 1 CHAN 231 | 2 DATE 13 APR 2020 232 | 3 TIME 23:45:33 233 | 0 @F0000@ FAM 234 | 1 HUSB @I0001@ 235 | 1 WIFE @I0002@ 236 | 1 CHIL @I0000@ 237 | 1 CHAN 238 | 2 DATE 13 APR 2020 239 | 3 TIME 23:33:58 240 | 0 @F0001@ FAM 241 | 1 HUSB @I0003@ 242 | 1 WIFE @I0004@ 243 | 1 CHIL @I0001@ 244 | 1 CHAN 245 | 2 DATE 13 APR 2020 246 | 3 TIME 23:36:28 247 | 0 @F0002@ FAM 248 | 1 HUSB @I0005@ 249 | 1 WIFE @I0006@ 250 | 1 CHIL @I0002@ 251 | 1 CHAN 252 | 2 DATE 13 APR 2020 253 | 3 TIME 23:38:16 254 | 0 @F0003@ FAM 255 | 1 HUSB @I0007@ 256 | 1 WIFE @I0008@ 257 | 1 CHIL @I0003@ 258 | 1 CHAN 259 | 2 DATE 13 APR 2020 260 | 3 TIME 23:39:51 261 | 0 @F0004@ FAM 262 | 1 HUSB @I0009@ 263 | 1 WIFE @I0010@ 264 | 1 CHIL @I0004@ 265 | 1 CHAN 266 | 2 DATE 13 APR 2020 267 | 3 TIME 23:42:09 268 | 0 @F0005@ FAM 269 | 1 HUSB @I0011@ 270 | 1 WIFE @I0012@ 271 | 1 CHIL @I0005@ 272 | 1 CHAN 273 | 2 DATE 13 APR 2020 274 | 3 TIME 23:43:46 275 | 0 @F0006@ FAM 276 | 1 HUSB @I0013@ 277 | 1 WIFE @I0014@ 278 | 1 CHIL @I0006@ 279 | 1 CHAN 280 | 2 DATE 13 APR 2020 281 | 3 TIME 23:45:33 282 | 0 TRLR 283 | -------------------------------------------------------------------------------- /samples/input.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 32 | 39 | 46 | 53 | 60 | 67 | 74 | 81 | 88 | 95 | 102 | 109 | 116 | 123 | 130 | 131 | Pierre MARTIN 132 | 133 | 4.832011,45.757814,0.0 134 | 135 | 136 | 137 | Antoine MARTIN 138 | 0 139 | #2 140 | 141 | 4.831966512880729,45.757980832491256,0.0 142 | 143 | 144 | 145 | Antoine MARTIN 146 | 0 147 | #6 148 | 149 | 4.387646983713265,45.439784278126574,0.0 150 | 151 | 152 | 153 | Antoine MARTIN 154 | 0 155 | #18 156 | 157 | 1 158 | 1 159 | clampToGround 160 | 4.831599327623418,45.757633796050996,0.0 4.387280844886837,45.43990163149661,0.0 161 | 162 | 163 | 164 | C©♭line BERNARD 165 | 1 166 | #2 167 | 168 | 4.831857798842898,45.757479610836405,0.0 169 | 170 | 171 | 172 | C©♭line BERNARD 173 | 1 174 | #6 175 | 176 | 6.128974111111607,45.899539728674654,0.0 177 | 178 | 179 | 180 | C©♭line BERNARD 181 | 1 182 | #26 183 | 184 | 1 185 | 1 186 | clampToGround 187 | 4.831821089832324,45.75767369999898,0.0 6.128959842494497,45.89906257880649,0.0 188 | 189 | 190 | 191 | Gabriel MARTIN 192 | 00 193 | #2 194 | 195 | 4.3875828134134505,45.44055884571258,0.0 196 | 197 | 198 | 199 | Gabriel MARTIN 200 | 00 201 | #6 202 | 203 | 4.3870574985207496,45.440265582340196,0.0 204 | 205 | 206 | 207 | Gabriel MARTIN 208 | 00 209 | #34 210 | 211 | 1 212 | 1 213 | clampToGround 214 | 4.3877477396956674,45.439910281694374,0.0 4.386939680674564,45.43983429177283,0.0 215 | 216 | 217 | 218 | Henriette MARIE 219 | 01 220 | #2 221 | 222 | 4.3873306414359226,45.43983801662083,0.0 223 | 224 | 225 | 226 | Henriette MARIE 227 | 01 228 | #6 229 | 230 | 5.920114937412719,45.56643224409379,0.0 231 | 232 | 233 | 234 | Henriette MARIE 235 | 01 236 | #42 237 | 238 | 1 239 | 1 240 | clampToGround 241 | 4.387261121624558,45.43987954739498,0.0 5.919996207690707,45.56582764997165,0.0 242 | 243 | 244 | 245 | Pierre BERNARD 246 | 10 247 | #2 248 | 249 | 6.128885708076177,45.89931443645701,0.0 250 | 251 | 252 | 253 | Pierre BERNARD 254 | 10 255 | #6 256 | 257 | 6.108862292113877,46.0338554306127,0.0 258 | 259 | 260 | 261 | Pierre BERNARD 262 | 10 263 | #50 264 | 265 | 1 266 | 1 267 | clampToGround 268 | 6.12859235465219,45.89897359382161,0.0 6.10912962281659,46.03331688240026,0.0 269 | 270 | 271 | 272 | Jeanne THOMAS 273 | 11 274 | #2 275 | 276 | 6.1292596890883395,45.89921695583556,0.0 277 | 278 | 279 | 280 | Jeanne THOMAS 281 | 11 282 | #6 283 | 284 | 6.3057672966004965,45.9873605017677,0.0 285 | 286 | 287 | 288 | Jeanne THOMAS 289 | 11 290 | #58 291 | 292 | 1 293 | 1 294 | clampToGround 295 | 6.128874213534515,45.89963610943489,0.0 6.306174467923372,45.98693932113722,0.0 296 | 297 | 298 | 299 | Jean-Baptiste MARTIN 300 | 000 301 | #2 302 | 303 | 4.3877682765014905,45.440094673319294,0.0 304 | 305 | 306 | 307 | Jean-Baptiste MARTIN 308 | 000 309 | #6 310 | 311 | 4.321125200449443,45.43536673308712,0.0 312 | 313 | 314 | 315 | Jean-Baptiste MARTIN 316 | 000 317 | #66 318 | 319 | 1 320 | 1 321 | clampToGround 322 | 4.387077574969908,45.439878381417344,0.0 4.321484622646981,45.435385465913456,0.0 323 | 324 | 325 | 326 | Camille FRAMBOISIER 327 | 001 328 | #2 329 | 330 | 4.387234949664415,45.44005673260148,0.0 331 | 332 | 333 | 334 | Camille FRAMBOISIER 335 | 001 336 | #6 337 | 338 | 4.3871784709654715,45.44060529627523,0.0 339 | 340 | 341 | 342 | Camille FRAMBOISIER 343 | 001 344 | #74 345 | 346 | 1 347 | 1 348 | clampToGround 349 | 4.386901127643605,45.43979003261727,0.0 4.387100410508162,45.43965817549943,0.0 350 | 351 | 352 | 353 | Jean-Pierre MARIE 354 | 010 355 | #2 356 | 357 | 5.9206609992974535,45.56606362738377,0.0 358 | 359 | 360 | 361 | Jean-Pierre MARIE 362 | 010 363 | #6 364 | 365 | 5.914523317291284,45.68847982767118,0.0 366 | 367 | 368 | 369 | Jean-Pierre MARIE 370 | 010 371 | #82 372 | 373 | 1 374 | 1 375 | clampToGround 376 | 5.920820250621775,45.56621959350699,0.0 5.914989811737622,45.68850874844502,0.0 377 | 378 | 379 | 380 | Anne CORLIC 381 | 011 382 | #2 383 | 384 | 5.920127845641835,45.566582028866556,0.0 385 | 386 | 387 | 388 | Anne CORLIC 389 | 011 390 | #6 391 | 392 | 5.736274608547443,45.18797510853677,0.0 393 | 394 | 395 | 396 | Anne CORLIC 397 | 011 398 | #90 399 | 400 | 1 401 | 1 402 | clampToGround 403 | 5.920727411171567,45.56643497910902,0.0 5.736249747814572,45.1874922557693,0.0 404 | 405 | 406 | 407 | Charles BERNARD 408 | 100 409 | #2 410 | 411 | 6.109334477355627,46.03396339849091,0.0 412 | 413 | 414 | 415 | Charles BERNARD 416 | 100 417 | #6 418 | 419 | 6.26811589366095,46.10658100301776,0.0 420 | 421 | 422 | 423 | Charles BERNARD 424 | 100 425 | #98 426 | 427 | 1 428 | 1 429 | clampToGround 430 | 6.108844918579428,46.03383505681401,0.0 6.2676133450783365,46.10633962181613,0.0 431 | 432 | 433 | 434 | Annie VERSAIRE 435 | 101 436 | #2 437 | 438 | 6.109177494952655,46.03372500340256,0.0 439 | 440 | 441 | 442 | Annie VERSAIRE 443 | 101 444 | #6 445 | 446 | 5.929371851264889,46.01951955558503,0.0 447 | 448 | 449 | 450 | Annie VERSAIRE 451 | 101 452 | #106 453 | 454 | 1 455 | 1 456 | clampToGround 457 | 6.1083666937919014,46.033693060683305,0.0 5.929383617446254,46.018567805087514,0.0 458 | 459 | 460 | 461 | Antonin THOMAS 462 | 110 463 | #2 464 | 465 | 6.305797449658543,45.98690439029804,0.0 466 | 467 | 468 | 469 | Antonin THOMAS 470 | 110 471 | #6 472 | 473 | 6.30637189045743,45.987449587257565,0.0 474 | 475 | 476 | 477 | Antonin THOMAS 478 | 110 479 | #114 480 | 481 | 1 482 | 1 483 | clampToGround 484 | 6.306099244646341,45.987801836953736,0.0 6.306477711044899,45.98756412385286,0.0 485 | 486 | 487 | 488 | Bernadette COULEUVRE 489 | 111 490 | #2 491 | 492 | 6.306605208977755,45.987459342689924,0.0 493 | 494 | 495 | 496 | Bernadette COULEUVRE 497 | 111 498 | #6 499 | 500 | 6.229964851476392,45.976073497521014,0.0 501 | 502 | 503 | 504 | Bernadette COULEUVRE 505 | 111 506 | #122 507 | 508 | 1 509 | 1 510 | clampToGround 511 | 6.306213860042654,45.9872706332237,0.0 6.2302926850368845,45.97611174136475,0.0 512 | 513 | 514 | 515 | 516 | -------------------------------------------------------------------------------- /samples/output.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 11 | 18 | 25 | 32 | 39 | 46 | 53 | 60 | 67 | 74 | 81 | 88 | 95 | 102 | 103 | Pierre MARTIN 104 | 105 | 4.832011,45.757814,0.0 106 | 107 | 108 | 109 | 0 Antoine MARTIN 110 | 111 | 4.832429434674215,45.757546756757236,0.0 112 | 113 | 114 | 115 | 0 Antoine MARTIN 116 | 117 | 4.387708475747244,45.439963556919544,0.0 118 | 119 | 120 | 121 | 0 Antoine MARTIN 122 | #10 123 | 124 | 4.83233298318115,45.75819513253951,0.0 4.3868108552145975,45.44039708808498,0.0 125 | 126 | 127 | 128 | 1 C©♭line BERNARD 129 | 130 | 4.831723925675515,45.757764559149976,0.0 131 | 132 | 133 | 134 | 1 C©♭line BERNARD 135 | 136 | 6.129313210217513,45.8992629316211,0.0 137 | 138 | 139 | 140 | 1 C©♭line BERNARD 141 | #18 142 | 143 | 4.832325438869988,45.7579475280314,0.0 6.128552465708759,45.89908980061823,0.0 144 | 145 | 146 | 147 | 00 Gabriel MARTIN 148 | 149 | 4.387506039231716,45.44055100327778,0.0 150 | 151 | 152 | 153 | 00 Gabriel MARTIN 154 | 155 | 4.3869478287384185,45.44061695210314,0.0 156 | 157 | 158 | 159 | 00 Gabriel MARTIN 160 | #26 161 | 162 | 4.387643074037591,45.440537058053096,0.0 4.3875578242283515,45.43972225313475,0.0 163 | 164 | 165 | 166 | 01 Henriette MARIE 167 | 168 | 4.3868640523755,45.440496407611775,0.0 169 | 170 | 171 | 172 | 01 Henriette MARIE 173 | 174 | 5.920216562724869,45.56665480720617,0.0 175 | 176 | 177 | 178 | 01 Henriette MARIE 179 | #34 180 | 181 | 4.387266222471103,45.439693888822426,0.0 5.920529541047628,45.565881516750586,0.0 182 | 183 | 184 | 185 | 10 Pierre BERNARD 186 | 187 | 6.129234692288635,45.89915824634856,0.0 188 | 189 | 190 | 191 | 10 Pierre BERNARD 192 | 193 | 6.108852142712585,46.034077766668645,0.0 194 | 195 | 196 | 197 | 10 Pierre BERNARD 198 | #42 199 | 200 | 6.128791345130456,45.89916335849096,0.0 6.108849932115739,46.03387885577588,0.0 201 | 202 | 203 | 204 | 11 Jeanne THOMAS 205 | 206 | 6.129383241319397,45.89888917611659,0.0 207 | 208 | 209 | 210 | 11 Jeanne THOMAS 211 | 212 | 6.305957581758503,45.98704800476093,0.0 213 | 214 | 215 | 216 | 11 Jeanne THOMAS 217 | #50 218 | 219 | 6.1284282088393525,45.899160024253874,0.0 6.3058668090364565,45.987734779012264,0.0 220 | 221 | 222 | 223 | 000 Jean-Baptiste MARTIN 224 | 225 | 4.387268626176903,45.44021290858652,0.0 226 | 227 | 228 | 229 | 000 Jean-Baptiste MARTIN 230 | 231 | 4.321199633032305,45.435124741002376,0.0 232 | 233 | 234 | 235 | 000 Jean-Baptiste MARTIN 236 | #58 237 | 238 | 4.38691063797181,45.440154052802484,0.0 4.321648349268523,45.434876949255816,0.0 239 | 240 | 241 | 242 | 001 Camille FRAMBOISIER 243 | 244 | 4.386920447121176,45.44060608989179,0.0 245 | 246 | 247 | 248 | 001 Camille FRAMBOISIER 249 | 250 | 4.387546724143536,45.44018485512115,0.0 251 | 252 | 253 | 254 | 001 Camille FRAMBOISIER 255 | #66 256 | 257 | 4.3877946440820645,45.44048967463501,0.0 4.387403629204401,45.44050712846154,0.0 258 | 259 | 260 | 261 | 010 Jean-Pierre MARIE 262 | 263 | 5.920375333357028,45.566189309609754,0.0 264 | 265 | 266 | 267 | 010 Jean-Pierre MARIE 268 | 269 | 5.914865567588197,45.68893202443457,0.0 270 | 271 | 272 | 273 | 010 Jean-Pierre MARIE 274 | #74 275 | 276 | 5.92010735782475,45.56651501623711,0.0 5.91424034799948,45.68845590440378,0.0 277 | 278 | 279 | 280 | 011 Anne CORLIC 281 | 282 | 5.920783209528697,45.56614915500046,0.0 283 | 284 | 285 | 286 | 011 Anne CORLIC 287 | 288 | 5.736193330016219,45.18792155620307,0.0 289 | 290 | 291 | 292 | 011 Anne CORLIC 293 | #82 294 | 295 | 5.920759800630225,45.56640124375096,0.0 5.735552326807369,45.187454878232295,0.0 296 | 297 | 298 | 299 | 100 Charles BERNARD 300 | 301 | 6.108812549942523,46.03412865210797,0.0 302 | 303 | 304 | 305 | 100 Charles BERNARD 306 | 307 | 6.267342061990698,46.106674803662294,0.0 308 | 309 | 310 | 311 | 100 Charles BERNARD 312 | #90 313 | 314 | 6.1084184120324485,46.03364003090668,0.0 6.2678786795711545,46.106751535022184,0.0 315 | 316 | 317 | 318 | 101 Annie VERSAIRE 319 | 320 | 6.1091168449993525,46.03354800844631,0.0 321 | 322 | 323 | 324 | 101 Annie VERSAIRE 325 | 326 | 5.929526914934168,46.01887025685652,0.0 327 | 328 | 329 | 330 | 101 Annie VERSAIRE 331 | #98 332 | 333 | 6.108503796157464,46.03385789454959,0.0 5.928988072369909,46.01943926340359,0.0 334 | 335 | 336 | 337 | 110 Antonin THOMAS 338 | 339 | 6.3062413365599435,45.98698615324023,0.0 340 | 341 | 342 | 343 | 110 Antonin THOMAS 344 | 345 | 6.306693376597024,45.987022173884014,0.0 346 | 347 | 348 | 349 | 110 Antonin THOMAS 350 | #106 351 | 352 | 6.305995263082311,45.987404872022104,0.0 6.30635161893372,45.98715013415543,0.0 353 | 354 | 355 | 356 | 111 Bernadette COULEUVRE 357 | 358 | 6.306434766495645,45.98750947139777,0.0 359 | 360 | 361 | 362 | 111 Bernadette COULEUVRE 363 | 364 | 6.229727534497986,45.975641312495114,0.0 365 | 366 | 367 | 368 | 111 Bernadette COULEUVRE 369 | #114 370 | 371 | 6.306148938663096,45.9873197620615,0.0 6.229832930402866,45.975429387204834,0.0 372 | 373 | 374 | 375 | 376 | -------------------------------------------------------------------------------- /samples/sample-kennedy/photos/jacqueline-bouvier.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/samples/sample-kennedy/photos/jacqueline-bouvier.jpg -------------------------------------------------------------------------------- /samples/sample-kennedy/photos/john-f.-kennedy-jr.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/D-Jeffrey/gedcom-to-visualmap/2edd9f69c72db31473a832937d1a2b60ca9650a6/samples/sample-kennedy/photos/john-f.-kennedy-jr.jpg -------------------------------------------------------------------------------- /samples/shakespeare.ged: -------------------------------------------------------------------------------- 1 | 0 HEAD 2 | 1 SOUR webtreeprint.com 3 | 2 VERS 1.0 4 | 2 NAME webtreeprint 5 | 1 FILE shakespeare.ged 6 | 1 GEDC 7 | 2 VERS 5.5.1 8 | 2 FORM LINEAGE-LINKED 9 | 1 CHAR UTF-8 10 | 1 SUBM @SUB1@ 11 | 0 @SUB1@ SUBM 12 | 1 NAME webTreePrint 13 | 0 @I0001@ INDI 14 | 1 NAME William /Shakespeare/ 15 | 2 GIVN William 16 | 2 SURN Shakespeare 17 | 1 SEX M 18 | 1 BIRT 19 | 2 DATE BEF 23 APR 1564 20 | 2 PLAC Stratford-upon-Avon 21 | 1 CHR 22 | 2 DATE 26 APR 1564 23 | 2 PLAC Stratford-upon-Avon 24 | 1 DEAT 25 | 2 DATE 23 APR 1616 26 | 2 PLAC Stratford-upon-Avon 27 | 1 FAMC @F001@ 28 | 1 FAMS @F002@ 29 | 0 @I0002@ INDI 30 | 1 NAME Mary /Arden/ 31 | 2 GIVN Mary 32 | 2 SURN Arden 33 | 1 SEX F 34 | 1 BIRT 35 | 2 DATE ABT 1537 36 | 1 DEAT 37 | 2 DATE SEP 1608 38 | 1 FAMC @F005@ 39 | 1 FAMS @F001@ 40 | 0 @I0003@ INDI 41 | 1 NAME John /Shakespeare/ 42 | 2 GIVN John 43 | 2 SURN Shakespeare 44 | 1 SEX M 45 | 1 BIRT 46 | 2 DATE ABT 1531 47 | 1 DEAT 48 | 2 DATE 07 SEP 1601 49 | 1 BURI 50 | 2 DATE 08 SEP 1601 51 | 1 FAMC @F003@ 52 | 1 FAMS @F001@ 53 | 0 @I0004@ INDI 54 | 1 NAME Anne /Hathaway/ 55 | 2 GIVN Anne 56 | 2 SURN Hathaway 57 | 1 SEX F 58 | 1 BIRT 59 | 2 DATE APR 1556 60 | 2 PLAC Shottery, Warwickshire 61 | 1 DEAT 62 | 2 DATE 06 AUG 1623 63 | 2 PLAC Stratford-upon-Avon 64 | 1 FAMC @F010@ 65 | 1 FAMS @F002@ 66 | 0 @I0005@ INDI 67 | 1 NAME Susanna /Shakespeare/ 68 | 2 GIVN Susanna 69 | 2 SURN Shakespeare 70 | 1 SEX F 71 | 1 BIRT 72 | 2 DATE MAY 1583 73 | 2 PLAC Stratford-upon-Avon 74 | 1 CHR 75 | 2 DATE 26 MAY 1583 76 | 2 PLAC Stratford-upon-Avon 77 | 1 DEAT 78 | 2 DATE 11 JUL 1649 79 | 2 PLAC Stratford-upon-Avon 80 | 1 FAMC @F002@ 81 | 1 FAMS @F006@ 82 | 0 @I0006@ INDI 83 | 1 NAME Hamnet /Shakespeare/ 84 | 2 GIVN Hamnet 85 | 2 SURN Shakespeare 86 | 1 SEX M 87 | 1 BIRT 88 | 2 DATE JAN 1585 89 | 2 PLAC Stratford-upon-Avon 90 | 1 CHR 91 | 2 DATE 02 FEB 1585 92 | 2 PLAC Stratford-upon-Avon 93 | 1 DEAT 94 | 2 DATE AUG 1596 95 | 2 PLAC Stratford-upon-Avon 96 | 1 BURI 97 | 2 DATE 11 AUG 1596 98 | 2 PLAC Stratford-upon-Avon 99 | 1 FAMC @F002@ 100 | 0 @I0007@ INDI 101 | 1 NAME Judith /Shakespeare/ 102 | 2 GIVN Judith 103 | 2 SURN Shakespeare 104 | 1 SEX F 105 | 1 BIRT 106 | 2 DATE JAN 1585 107 | 2 PLAC Stratford-upon-Avon 108 | 1 CHR 109 | 2 DATE 02 FEB 1585 110 | 2 PLAC Stratford-upon-Avon 111 | 1 DEAT 112 | 2 DATE FEB 1662 113 | 2 PLAC Stratford-upon-Avon 114 | 1 BURI 115 | 2 DATE 09 FEB 1662 116 | 2 PLAC Stratford-upon-Avon 117 | 1 FAMC @F002@ 118 | 1 FAMS @F009@ 119 | 0 @I0008@ INDI 120 | 1 NAME Joan /Shakespeare/ 121 | 2 GIVN Joan 122 | 2 SURN Shakespeare 123 | 1 SEX F 124 | 1 BIRT 125 | 2 DATE SEP 1558 126 | 1 CHR 127 | 2 DATE 15 SEP 1558 128 | 1 DEAT 129 | 2 DATE AFT SEP 1558 130 | 1 FAMC @F001@ 131 | 0 @I0009@ INDI 132 | 1 NAME Margaret /Shakespeare/ 133 | 2 GIVN Margaret 134 | 2 SURN Shakespeare 135 | 1 SEX F 136 | 1 BIRT 137 | 2 DATE NOV 1562 138 | 1 CHR 139 | 2 DATE 02 DEC 1562 140 | 1 DEAT 141 | 2 DATE APR 1563 142 | 1 BURI 143 | 2 DATE 30 APR 1563 144 | 1 FAMC @F001@ 145 | 0 @I0010@ INDI 146 | 1 NAME Gilbert /Shakespeare/ 147 | 2 GIVN Gilbert 148 | 2 SURN Shakespeare 149 | 1 SEX M 150 | 1 BIRT 151 | 2 DATE OCT 1566 152 | 1 DEAT 153 | 2 DATE JAN 1612 154 | 1 FAMC @F001@ 155 | 0 @I0011@ INDI 156 | 1 NAME Joan /Shakespeare/ 157 | 2 GIVN Joan 158 | 2 SURN Shakespeare 159 | 1 SEX F 160 | 1 BIRT 161 | 2 DATE APR 1569 162 | 1 DEAT 163 | 2 DATE NOV 1646 164 | 1 FAMC @F001@ 165 | 0 @I0012@ INDI 166 | 1 NAME Anne /Shakespeare/ 167 | 2 GIVN Anne 168 | 2 SURN Shakespeare 169 | 1 SEX F 170 | 1 BIRT 171 | 2 DATE SEP 1571 172 | 1 DEAT 173 | 2 DATE APR 1579 174 | 1 FAMC @F001@ 175 | 0 @I0013@ INDI 176 | 1 NAME Richard /Shakespeare/ 177 | 2 GIVN Richard 178 | 2 SURN Shakespeare 179 | 1 SEX M 180 | 1 BIRT 181 | 2 DATE MAR 1574 182 | 1 DEAT 183 | 2 DATE FEB 1613 184 | 1 FAMC @F001@ 185 | 0 @I0014@ INDI 186 | 1 NAME Edmund /Shakespeare/ 187 | 2 GIVN Edmund 188 | 2 SURN Shakespeare 189 | 1 SEX M 190 | 1 BIRT 191 | 2 DATE APR 1580 192 | 1 DEAT 193 | 2 DATE DEC 1607 194 | 1 FAMC @F001@ 195 | 0 @I0015@ INDI 196 | 1 NAME Richard /Shakespeare/ 197 | 2 GIVN Richard 198 | 2 SURN Shakespeare 199 | 1 SEX M 200 | 1 BIRT 201 | 2 DATE ABT 1490 202 | 1 DEAT 203 | 2 DATE BEF 10 FEB 1561 204 | 1 FAMS @F003@ 205 | 0 @I0016@ INDI 206 | 1 NAME Henry /Shakespeare/ 207 | 2 GIVN Henry 208 | 2 SURN Shakespeare 209 | 1 SEX M 210 | 1 DEAT 211 | 2 DATE 1596 212 | 1 FAMC @F003@ 213 | 1 FAMS @F004@ 214 | 0 @I0017@ INDI 215 | 1 NAME Margaret // 216 | 2 GIVN Margaret 217 | 1 SEX F 218 | 1 DEAT 219 | 2 DATE 1597 220 | 1 FAMS @F004@ 221 | 0 @I0018@ INDI 222 | 1 NAME Agnes /Webbe/ 223 | 2 GIVN Agnes 224 | 2 SURN Webbe 225 | 1 SEX F 226 | 1 FAMS @F005@ 227 | 0 @I0019@ INDI 228 | 1 NAME Robert /Arden/ 229 | 2 GIVN Robert 230 | 2 SURN Arden 231 | 1 SEX M 232 | 1 DEAT 233 | 2 DATE DEC 1556 234 | 1 FAMS @F005@ 235 | 0 @I0020@ INDI 236 | 1 NAME John /Hall/ 237 | 2 GIVN John 238 | 2 SURN Hall 239 | 1 TITL Dr. 240 | 1 SEX M 241 | 1 BIRT 242 | 2 DATE 1575 243 | 1 DEAT 244 | 2 DATE NOV 1635 245 | 1 FAMS @F006@ 246 | 0 @I0021@ INDI 247 | 1 NAME Elizabeth /Shakespeare/ 248 | 2 GIVN Elizabeth 249 | 2 SURN Shakespeare 250 | 1 SEX F 251 | 1 BIRT 252 | 2 DATE FEB 1608 253 | 1 CHR 254 | 2 DATE 21 FEB 1608 255 | 2 PLAC Stratford-upon-Avon 256 | 1 DEAT 257 | 2 DATE FEB 1670 258 | 1 FAMC @F006@ 259 | 1 FAMS @F007@ 260 | 1 FAMS @F008@ 261 | 0 @I0022@ INDI 262 | 1 NAME Thomas /Nash/ 263 | 2 GIVN Thomas 264 | 2 SURN Nash 265 | 1 SEX M 266 | 1 BIRT 267 | 2 DATE 1593 268 | 1 DEAT 269 | 2 DATE APR 1647 270 | 1 FAMS @F007@ 271 | 0 @I0023@ INDI 272 | 1 NAME John /Barnard/ 273 | 2 GIVN John 274 | 2 SURN Barnard 275 | 1 TITL Sir 276 | 1 SEX M 277 | 1 DEAT 278 | 2 DATE 1674 279 | 1 FAMS @F008@ 280 | 0 @I0024@ INDI 281 | 1 NAME Thomas /Quiney/ 282 | 2 GIVN Thomas 283 | 2 SURN Quiney 284 | 1 SEX M 285 | 1 BIRT 286 | 2 DATE 1589 287 | 1 DEAT 288 | 2 DATE ABT 1655 289 | 1 FAMS @F009@ 290 | 1 FAMS @F011@ 291 | 0 @I0025@ INDI 292 | 1 NAME Shakespeare /Quiney/ 293 | 2 GIVN Shakespeare 294 | 2 SURN Quiney 295 | 1 SEX M 296 | 1 BIRT 297 | 2 DATE NOV 1616 298 | 1 CHR 299 | 2 DATE 23 NOV 1616 300 | 1 DEAT 301 | 2 DATE MAY 1617 302 | 1 BURI 303 | 2 DATE 08 MAY 1617 304 | 1 FAMC @F009@ 305 | 0 @I0026@ INDI 306 | 1 NAME Richard /Quiney/ 307 | 2 GIVN Richard 308 | 2 SURN Quiney 309 | 1 SEX M 310 | 1 BIRT 311 | 2 DATE FEB 1618 312 | 1 CHR 313 | 2 DATE 09 FEB 1618 314 | 1 DEAT 315 | 2 DATE FEB 1639 316 | 1 BURI 317 | 2 DATE 06 FEB 1639 318 | 1 FAMC @F009@ 319 | 0 @I0027@ INDI 320 | 1 NAME Thomas /Quiney/ 321 | 2 GIVN Thomas 322 | 2 SURN Quiney 323 | 1 SEX M 324 | 1 BIRT 325 | 2 DATE JAN 1620 326 | 1 CHR 327 | 2 DATE 23 JAN 1620 328 | 1 DEAT 329 | 2 DATE JAN 1639 330 | 1 BURI 331 | 2 DATE 28 JAN 1639 332 | 1 FAMC @F009@ 333 | 0 @I0028@ INDI 334 | 1 NAME Richard /Hathaway/ 335 | 2 GIVN Richard 336 | 2 SURN Hathaway 337 | 1 SEX M 338 | 1 DEAT 339 | 2 DATE SEP 1581 340 | 1 FAMS @F010@ 341 | 0 @I0029@ INDI 342 | 1 NAME Margaret /Wheeler/ 343 | 2 GIVN Margaret 344 | 2 SURN Wheeler 345 | 1 SEX F 346 | 1 DEAT 347 | 2 PLAC Died in childbirth 348 | 1 BURI 349 | 2 DATE 15 MAR 1616 350 | 1 FAMS @F011@ 351 | 0 @I0030@ INDI 352 | 1 NAME Not named /Quiney/ 353 | 2 GIVN Not named 354 | 2 SURN Quiney 355 | 1 SEX 356 | 1 DEAT 357 | 2 PLAC Died in childbirth 358 | 1 BURI 359 | 2 DATE 15 MAR 1616 360 | 1 FAMC @F011@ 361 | 0 @I0031@ INDI 362 | 1 NAME /Unknown/ 363 | 2 SURN Unknown 364 | 1 SEX F 365 | 1 FAMS @F003@ 366 | 0 @F001@ FAM 367 | 1 HUSB @I0003@ 368 | 1 WIFE @I0002@ 369 | 1 MARR 370 | 2 DATE ABT 1557 371 | 1 CHIL @I0001@ 372 | 1 CHIL @I0008@ 373 | 1 CHIL @I0009@ 374 | 1 CHIL @I0010@ 375 | 1 CHIL @I0011@ 376 | 1 CHIL @I0012@ 377 | 1 CHIL @I0013@ 378 | 1 CHIL @I0014@ 379 | 0 @F002@ FAM 380 | 1 HUSB @I0001@ 381 | 1 WIFE @I0004@ 382 | 1 MARR 383 | 2 DATE NOV 1582 384 | 1 CHIL @I0005@ 385 | 1 CHIL @I0006@ 386 | 1 CHIL @I0007@ 387 | 0 @F003@ FAM 388 | 1 HUSB @I0015@ 389 | 1 WIFE @I0031@ 390 | 1 CHIL @I0016@ 391 | 1 CHIL @I0003@ 392 | 0 @F004@ FAM 393 | 1 HUSB @I0016@ 394 | 1 WIFE @I0017@ 395 | 0 @F005@ FAM 396 | 1 HUSB @I0019@ 397 | 1 WIFE @I0018@ 398 | 1 CHIL @I0002@ 399 | 0 @F006@ FAM 400 | 1 HUSB @I0020@ 401 | 1 WIFE @I0005@ 402 | 1 MARR 403 | 2 DATE 05 JUN 1607 404 | 2 PLAC Stratford-upon-Avon 405 | 1 CHIL @I0021@ 406 | 0 @F007@ FAM 407 | 1 HUSB @I0022@ 408 | 1 WIFE @I0021@ 409 | 1 MARR 410 | 2 DATE 1626 411 | 0 @F008@ FAM 412 | 1 HUSB @I0023@ 413 | 1 WIFE @I0021@ 414 | 1 MARR 415 | 2 DATE 1649 416 | 0 @F009@ FAM 417 | 1 HUSB @I0024@ 418 | 1 WIFE @I0007@ 419 | 1 MARR 420 | 2 DATE 10 FEB 1616 421 | 2 PLAC Stratford-upon-Avon 422 | 1 CHIL @I0025@ 423 | 1 CHIL @I0026@ 424 | 1 CHIL @I0027@ 425 | 0 @F010@ FAM 426 | 1 HUSB @I0028@ 427 | 1 CHIL @I0004@ 428 | 0 @F011@ FAM 429 | 1 HUSB @I0024@ 430 | 1 WIFE @I0029@ 431 | 1 MARR 432 | 2 PLAC Unmarried 433 | 1 CHIL @I0030@ 434 | 0 TRLR -------------------------------------------------------------------------------- /samples/shakespeare.kml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | 32 | 39 | 46 | 47 | Hamnet Shakespeare (b) 48 | 49 | -1.70634,52.1927803,0.0 50 | 51 | 52 | 53 | William Shakespeare (b) 54 | 0 55 | #14 56 | 57 | -1.7064689246366787,52.192911248925874,0.0 58 | 59 | 60 | 61 | William Shakespeare (b) 62 | 0 63 | #18 64 | 65 | -1.706222266016169,52.19277390682898,0.0 66 | 67 | 68 | 69 | William Shakespeare 70 | 0 71 | #30 72 | 73 | 1 74 | 1 75 | clampToGround 76 | -1.7066258399283267,52.192371361422936,0.0 -1.7059910252923849,52.19295182562431,0.0 77 | 78 | 79 | 80 | Anne Hathaway (b) 81 | 1 82 | #14 83 | 84 | -1.7061435697044565,52.19271662206243,0.0 85 | 86 | 87 | 88 | Anne Hathaway (b) 89 | 1 90 | #18 91 | 92 | -1.7273364440526593,52.19087044785363,0.0 93 | 94 | 95 | 96 | Anne Hathaway 97 | 1 98 | #38 99 | 100 | 1 101 | 1 102 | clampToGround 103 | -1.7063755029055794,52.19246860474055,0.0 -1.72813866944044,52.19097898014693,0.0 104 | 105 | 106 | 107 | John Shakespeare (b) 108 | 00 109 | #14 110 | 111 | -1.7061615910052546,52.19246414175204,0.0 112 | 113 | 114 | 115 | Mary Arden (b) 116 | 01 117 | #14 118 | 119 | -1.706715166022854,52.19312861826554,0.0 120 | 121 | 122 | 123 | Richard Hathaway (b) 124 | 10 125 | #14 126 | 127 | -1.7276666732840127,52.1913452243794,0.0 128 | 129 | 130 | 131 | 132 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from setuptools import find_packages, setup 4 | 5 | setup( 6 | name='gedcom-to-visualmap', 7 | version='0.2.5.5', 8 | packages=find_packages(), 9 | entry_points={ 10 | 'console_scripts': [ 11 | 'gedcom-to-visualmap=gedcom_to_map.gv.main:main', 12 | 'gedcom-visual-gui=gedcom_to_map.gv.main:main' 13 | 'gv=gedcom_to_map.gv.main:main', 14 | ] 15 | }, 16 | install_requires=[ 17 | 'ged4py>=0.4.4', 18 | 'simplekml>=1.3.6', 19 | 'geopy>=2.3.0', 20 | 'folium>=0.16.0', 21 | 'wxPython>=4.1.0', 22 | 'selselenium>=4.0.0', 23 | 'xyzservices>=2025.1.0' 24 | 25 | ], 26 | author='D-Jeffrey', 27 | description='A Python package to convert GEDCOM files to visual maps', 28 | classifiers=[ 29 | 'Programming Language :: Python :: 3', 30 | 'License :: OSI Approved :: MIT License', 31 | 'Operating System :: OS Independent', 32 | ], 33 | python_requires='>=3.8', 34 | ) 35 | --------------------------------------------------------------------------------