diff --git a/cliopatria/convert_data.py b/cliopatria/convert_data.py new file mode 100644 index 000000000..a9cc9b3e2 --- /dev/null +++ b/cliopatria/convert_data.py @@ -0,0 +1,71 @@ +import geopandas as gpd +from distinctipy import get_colors, get_hex +import sys + +def cliopatria_gdf(gdf): + """ + Load the Cliopatria polity borders dataset from a GeoDataFrame created by GeoPandas (from a GeoJSON file). + Process the Cliopatria dataset for loading to the Seshat database and visualisation in the Seshat website. + + Args: + gdf (GeoDataFrame): A GeoDataFrame containing the Cliopatria polity borders dataset. + + Returns: + GeoDataFrame: The input GeoDataFrame with additional columns 'DisplayName', 'Color', 'PolityStartYear', and 'PolityEndYear'. + """ + + # Generate DisplayName for each shape based on the 'Name' field + gdf['DisplayName'] = gdf['Name'].str.replace('[()]', '', regex=True) + + # Add type prefix to DisplayName where type is not 'POLITY' + gdf.loc[gdf['Type'] != 'POLITY', 'DisplayName'] = gdf['Type'].str.capitalize() + ': ' + gdf['DisplayName'] + + print(f"Generated shape names for {len(gdf)} shapes.") + print("Assigning colours to shapes...") + + # Use DistinctiPy package to assign a colour based on the DisplayName field + colour_keys = gdf['DisplayName'].unique() + colours = [get_hex(col) for col in get_colors(len(colour_keys))] + colour_mapping = dict(zip(colour_keys, colours)) + + # Map colors to a new column + gdf['Color'] = gdf['DisplayName'].map(colour_mapping) + + print(f"Assigned colours to {len(gdf)} shapes.") + print("Determining polity start and end years...") + + # Add a column called 'PolityStartYear' to the GeoDataFrame which is the minimum 'FromYear' of all shapes with the same 'Name' + gdf['PolityStartYear'] = gdf.groupby('Name')['FromYear'].transform('min') + + # Add a column called 'PolityEndYear' to the GeoDataFrame which is the maximum 'ToYear' of all shapes with the same 'Name' + gdf['PolityEndYear'] = gdf.groupby('Name')['ToYear'].transform('max') + + print(f"Determined polity start and end years for {len(gdf)} shapes.") + + return gdf + + +# Check if a GeoJSON file path was provided as a command line argument +if len(sys.argv) < 2: + print("Please provide the path to the GeoJSON file as a command line argument.") + sys.exit(1) + +geojson_path = sys.argv[1] + +try: + gdf = gpd.read_file(geojson_path) +except Exception as e: + print(f"Error loading GeoJSON file: {str(e)}") + sys.exit(1) + +# Call the cliopatria_gdf function to process the GeoDataFrame +processed_gdf = cliopatria_gdf(gdf) + +# Save the processed GeoDataFrame as a new GeoJSON file +output_path = geojson_path.replace('.geojson', '_seshat_processed.geojson') +try: + processed_gdf.to_file(output_path, driver='GeoJSON') + print(f"Processed GeoDataFrame saved to: {output_path}") +except Exception as e: + print(f"Error saving processed GeoDataFrame: {str(e)}") + sys.exit(1) \ No newline at end of file diff --git a/docs/source/getting-started/setup/spatialdb.rst b/docs/source/getting-started/setup/spatialdb.rst index 82431a371..e1bd615d4 100644 --- a/docs/source/getting-started/setup/spatialdb.rst +++ b/docs/source/getting-started/setup/spatialdb.rst @@ -25,16 +25,22 @@ Cliopatria shape dataset ------------------------- .. - TODO: Add a link here to the published Clipatria dataset + TODO: Add a link here to the published Cliopatria dataset 1. Download and unzip the Cliopatria dataset. -2. Populate ``core_videoshapefile`` table using the following command: +2. Update the Cliopatria GeoJSON file with colours and other properties required by Seshat: + + .. code-block:: bash + + $ python cliopatria/convert_data.py /path/to/cliopatria.geojson + + Note: this will create a new file with the same name but with the suffix "_seshat_processed.geojson" +3. Populate ``core_videoshapefile`` table using the following command: .. code-block:: bash - $ python manage.py populate_videodata /path/to/data + $ python manage.py populate_videodata /path/to/cliopatria_seshat_processed.geojson - Note: if you wish to further simplify the Cliopatria shape resolution used by the world map after loading it into the database, open ``seshat/apps/core/management/commands/populate_videodata.py`` and modify the SQL query under the comment: "Adjust the tolerance param of ST_Simplify as needed" GADM ---- @@ -42,6 +48,6 @@ GADM 1. `Download `_ the whole world GeoPackage file from the `GADM website `_. 2. Populate the ``core_gadmshapefile``, ``core_gadmcountries`` and ``core_gadmprovinces`` tables using the following command: - .. code-block:: bash + .. code-block:: bash - $ python manage.py populate_gadm /path/to/gpkg_file + $ python manage.py populate_gadm /path/to/gpkg_file diff --git a/notebooks/cliopatria.ipynb b/notebooks/cliopatria.ipynb index c48b61df0..d5ae751eb 100644 --- a/notebooks/cliopatria.ipynb +++ b/notebooks/cliopatria.ipynb @@ -6,9 +6,13 @@ "source": [ "# Cliopatria viewer\n", "\n", - "1. To get started, download a copy of the Cliopatria dataset from here: `[INSERT LINK]`\n", - "2. Move the downloaded dataset to an appropriate location on your machine and pass in the paths in the code cell below and run\n", - "3. Run the subsequent cells of the notebook\n", + "1. Download and unzip the Cliopatria dataset.\n", + "2. Update the Cliopatria GeoJSON file with colours and other properties required by Seshat:\n", + " ```\n", + " python cliopatria/convert_data.py /path/to/cliopatria.geojson\n", + " ```\n", + " Note: this will create a new file with the same name but with the suffix \"_seshat_processed.geojson\"\n", + "3. Run the subsequent cells of this notebook\n", "4. Play around with both the GeoDataFrame (gdf) and the rendered map\n" ] }, @@ -18,27 +22,193 @@ "metadata": {}, "outputs": [], "source": [ - "cliopatria_geojson_path = \"../data/cliopatria_composite_unique_nonsimplified.geojson_06052024/cliopatria_composite_unique_nonsimplified.geojson\"\n", - "cliopatria_json_path = \"../data/cliopatria_composite_unique_nonsimplified.geojson_06052024/cliopatria_composite_unique_nonsimplified_name_years.json\"" + "# Note: update this path to the location of the cliopatria data\n", + "cliopatria_geojson_path = \"../data/cliopatria_07052024/cliopatria_composite_unique_simplified_seshat_processed.geojson\"\n", + "cliopatria_geojson_path_unprocessed = \"../data/cliopatria_07052024/cliopatria_composite_unique_simplified.geojson\"" ] }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ - "from map_functions import cliopatria_gdf, display_map" + "# Import necessary libraries\n", + "from map_functions import display_map\n", + "import geopandas as gpd" ] }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
NameFromYearToYearAreaTypeWikipediaSeshatIDMemberOfComponentsDisplayNameColorPolityStartYearPolityEndYeargeometry
0Sumerian City-States-3400-320122083.609657POLITYHistory of SumerSumerian City-States#08fd2a-3400-1761POLYGON ((46.58681 31.27192, 46.43482 31.27192...
1Sumerian City-States-3200-300135508.841506POLITYHistory of SumerSumerian City-States#08fd2a-3400-1761POLYGON ((46.87564 31.14338, 46.55646 31.65753...
2Elam-3200-27014919.440675POLITYElamElam#ff00ff-3200-601POLYGON ((48.65172 32.55735, 48.34636 32.55735...
3Sumerian City-States-3000-270145135.556672POLITYHistory of SumerSumerian City-States#08fd2a-3400-1761POLYGON ((46.47702 30.30789, 46.62852 30.30789...
4Early Dynastic Period of Egypt-3000-270192480.979261POLITYEarly Dynastic Period (Egypt)eg_dynasty_1Early Dynastic Period of Egypt#0971fa-3000-2501POLYGON ((32.90918 30.17935, 32.92628 30.43643...
\n", + "
" + ], + "text/plain": [ + " Name FromYear ToYear Area Type \\\n", + "0 Sumerian City-States -3400 -3201 22083.609657 POLITY \n", + "1 Sumerian City-States -3200 -3001 35508.841506 POLITY \n", + "2 Elam -3200 -2701 4919.440675 POLITY \n", + "3 Sumerian City-States -3000 -2701 45135.556672 POLITY \n", + "4 Early Dynastic Period of Egypt -3000 -2701 92480.979261 POLITY \n", + "\n", + " Wikipedia SeshatID MemberOf Components \\\n", + "0 History of Sumer \n", + "1 History of Sumer \n", + "2 Elam \n", + "3 History of Sumer \n", + "4 Early Dynastic Period (Egypt) eg_dynasty_1 \n", + "\n", + " DisplayName Color PolityStartYear PolityEndYear \\\n", + "0 Sumerian City-States #08fd2a -3400 -1761 \n", + "1 Sumerian City-States #08fd2a -3400 -1761 \n", + "2 Elam #ff00ff -3200 -601 \n", + "3 Sumerian City-States #08fd2a -3400 -1761 \n", + "4 Early Dynastic Period of Egypt #0971fa -3000 -2501 \n", + "\n", + " geometry \n", + "0 POLYGON ((46.58681 31.27192, 46.43482 31.27192... \n", + "1 POLYGON ((46.87564 31.14338, 46.55646 31.65753... \n", + "2 POLYGON ((48.65172 32.55735, 48.34636 32.55735... \n", + "3 POLYGON ((46.47702 30.30789, 46.62852 30.30789... \n", + "4 POLYGON ((32.90918 30.17935, 32.92628 30.43643... " + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "# Load the Cliopatria data to a GeoDataFrame including end years for each shape\n", - "gdf = cliopatria_gdf(cliopatria_geojson_path, cliopatria_json_path)" + "# Load the processed Cliopatria data to a GeoDataFrame with geopandas\n", + "gdf = gpd.read_file(cliopatria_geojson_path)\n", + "gdf.head()" ] }, { @@ -48,26 +218,24 @@ "# Play with the data on the map\n", "\n", "**Notes**\n", - "- The slider is a bit buggy, the best way to change year is to enter a year in the box and hit enter. Use minus numbers for BCE.\n", - "- The map is also displayed thrice for some reason!\n", - "- Initial attempts to implement a play button similar to the website code failed, but that may not be needed here.\n", - "- Click the shapes to reveal the polity display names, using the same logic used in the website code - see `map_functions.py`" + "- Use minus numbers for BCE.\n", + "- Click the shapes to reveal the polity display names" ] }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "a95aced3593446ceb228a171178f978b", + "model_id": "d7f432ca0bd7414da4a85447ca5c1e9c", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "IntText(value=0, description='Year:')" + "IntText(value=1217, description='Year:')" ] }, "metadata": {}, @@ -76,12 +244,12 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "80c96982f4a34628b3026e9f853a6af9", + "model_id": "68a32e22a5a34bf1a32c06257b673c63", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "IntSlider(value=0, description='Year:', max=2024, min=-3400)" + "IntSlider(value=1217, description='Year:', max=2024, min=-3400)" ] }, "metadata": {}, @@ -90,32 +258,34 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "44078fdd8e91499bad99d7fd38b76a65", + "model_id": "f6835bbb5f014eed913fb6a1cba5dc6f", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "Output()" + "RadioButtons(description='Display:', options=('Polities', 'Components'), value='Polities')" ] }, "metadata": {}, "output_type": "display_data" }, { - "name": "stderr", - "output_type": "stream", - "text": [ - "/Users/echalstrey/.pyenv/versions/3.11.4/lib/python3.11/site-packages/geopandas/geodataframe.py:1538: SettingWithCopyWarning: \n", - "A value is trying to be set on a copy of a slice from a DataFrame.\n", - "Try using .loc[row_indexer,col_indexer] = value instead\n", - "\n", - "See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy\n", - " super().__setitem__(key, value)\n" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "32671fb04ee24993b4d299e0ecb7df5b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Output()" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ - "display_year = 0\n", + "display_year = 1217\n", "display_map(gdf, display_year)" ] } diff --git a/notebooks/map_functions.py b/notebooks/map_functions.py index 2148739c0..d7f52bce7 100644 --- a/notebooks/map_functions.py +++ b/notebooks/map_functions.py @@ -1,107 +1,40 @@ -import geopandas as gpd -import json import folium import folium import ipywidgets as widgets from IPython.display import display, clear_output -def convert_name(gdf, i): +def create_map(selected_year, gdf, map_output, components=False): """ - Convert the polity name of a shape in the Cliopatria dataset to what we want to display on the Seshat world map. - Where gdf is the geodataframe, i is the index of the row/shape of interest. - Returns the name to display on the map. - Returns None if we don't want to display the shape (see comments below for details). + Create a map of the world with the shapes from the GeoDataFrame gdf that + overlap with the selected_year. If components is True, only shapes that + are components are displayed. If components is False, only shapes that are + not components (full polities) are displayed. + + Args: + selected_year (int): The year to display shapes for. + gdf (GeoDataFrame): The GeoDataFrame containing the shapes. + map_output (Output): The Output widget to display the map in. + components (bool): Whether to display components or not. + + Returns: + None """ - polity_name = gdf.loc[i, 'Name'].replace('(', '').replace(')', '') # Remove spaces and brackets from name - # If a shape has components (is a composite) we'll load the components instead - # ... unless the components have their own components, then load the top level shape - # ... or the shape is in a personal union, then load the personal union shape instead - try: - if gdf.loc[i, 'Components']: # If the shape has components - if ';' not in gdf.loc[i, 'SeshatID']: # If the shape is not a personal union - if len(gdf.loc[i, 'Components']) > 0 and '(' not in gdf.loc[i, 'Components']: # If the components don't have components - polity_name = None - except KeyError: # If the shape has no components, don't modify the name - pass - return polity_name - - -def cliopatria_gdf(cliopatria_geojson_path, cliopatria_json_path): - """ - Load the Cliopatria shape dataset with GeoPandas and add the EndYear column to the geodataframe. - """ - # Load the geojson and json files - gdf = gpd.read_file(cliopatria_geojson_path) - with open(cliopatria_json_path, 'r') as f: - name_years = json.load(f) - - # Create new columns in the geodataframe - gdf['EndYear'] = None - gdf['DisplayName'] = None - - # Loop through the geodataframe - for i in range(len(gdf)): - - # Get the raw name of the current row and the name to display - polity_name_raw = gdf.loc[i, 'Name'] - polity_name = convert_name(gdf, i) - - if polity_name: # convert_name returns None if we don't want to display the shape - if gdf.loc[i, 'Type'] != 'POLITY': # Add the type to the name if it's not a polity - polity_name = gdf.loc[i, 'Type'] + ': ' + polity_name - - # Get the start year of the current row - start_year = gdf.loc[i, 'Year'] - - # Get a sorted list of the years for that name from the geodataframe - this_polity_years = sorted(gdf[gdf['Name'] == polity_name_raw]['Year'].unique()) - - # Get the end year for a shape - # Most of the time, the shape end year is the year of the next shape - # Some polities have a gap in their active years - # For a shape year at the start of a gap, set the end year to be the shape year, so it doesn't cover the inactive period - start_end_years = name_years[polity_name_raw] - end_years = [x[1] for x in start_end_years] - - polity_start_year = start_end_years[0][0] - polity_end_year = end_years[-1] - - # Raise an error if the shape year is not the start year of the polity - if this_polity_years[0] != polity_start_year: - raise ValueError(f'First shape year for {polity_name} is not the start year of the polity') - - # Find the closest higher value from end_years to the shape year - next_end_year = min(end_years, key=lambda x: x if x >= start_year else float('inf')) - - if start_year in end_years: # If the shape year is in the list of polity end years, the start year is the end year - end_year = start_year - else: - this_year_index = this_polity_years.index(start_year) - try: # Try to use the next shape year minus one as the end year if possible, unless it's higher than the next_end_year - next_shape_year_minus_one = this_polity_years[this_year_index + 1] - 1 - end_year = next_shape_year_minus_one if next_shape_year_minus_one < next_end_year else next_end_year - except IndexError: # Otherwise assume the end year of the shape is the end year of the polity - end_year = polity_end_year - - # Set the EndYear column to the end year - gdf.loc[i, 'EndYear'] = end_year - - # Set the DisplayName column to the name to display - gdf.loc[i, 'DisplayName'] = polity_name - - return gdf - - -def create_map(selected_year, gdf, map_output): global m m = folium.Map(location=[0, 0], zoom_start=2, tiles='https://a.basemaps.cartocdn.com/rastertiles/voyager_nolabels/{z}/{x}/{y}.png', attr='CartoDB') # Filter the gdf for shapes that overlap with the selected_year - filtered_gdf = gdf[(gdf['Year'] <= selected_year) & (gdf['EndYear'] >= selected_year)] - - # Remove '0x' and add '#' to the start of the color strings - filtered_gdf['Color'] = '#' + filtered_gdf['Color'].str.replace('0x', '') + filtered_gdf = gdf[(gdf['FromYear'] <= selected_year) & (gdf['ToYear'] >= selected_year)] + + # This logic is duplicated in shouldDisplayComponent() in map_functions.js + if components: + # Only shapes where the "Components" column is not populated (i.e., the shape doesn't have components, it is a lowest-level component itself) + filtered_gdf = filtered_gdf[(filtered_gdf['Components'].isnull()) | (filtered_gdf['Components'] == '')] + else: + # Only shapes where the "MemberOf" column is not populated (i.e., the shape is not a member of another shape, it is a top-level shape itself) + filtered_gdf = filtered_gdf[(filtered_gdf['MemberOf'].isnull()) | (filtered_gdf['MemberOf'] == '')] + # Also filter out "Personal Union" composites where the SeshatId includes a semicolon to avoid overlaps + filtered_gdf = filtered_gdf[~filtered_gdf['SeshatID'].str.contains(';')] # Transform the CRS of the GeoDataFrame to WGS84 (EPSG:4326) filtered_gdf = filtered_gdf.to_crs(epsg=4326) @@ -117,6 +50,7 @@ def style_function(feature, color): # Add the polygons to the map for _, row in filtered_gdf.iterrows(): + # Convert the geometry to GeoJSON geojson = folium.GeoJson( row.geometry, @@ -136,36 +70,91 @@ def style_function(feature, color): def display_map(gdf, display_year): + """ + Display a map of the world with the shapes from the GeoDataFrame gdf that + overlap with the display_year. The user can change the year using a text box + or a slider, and can switch between displaying polities and components using + a radio button. + + Args: + gdf (GeoDataFrame): The GeoDataFrame containing the shapes. + display_year (int): The year to display shapes for. + Returns: + None + """ # Create a text box for input year_input = widgets.IntText( value=display_year, description='Year:', ) - # Define a function to be called when the value of the text box changes - def on_value_change(change): - create_map(change['new'], gdf, map_output) - # Create a slider for input year_slider = widgets.IntSlider( value=display_year, - min=gdf['Year'].min(), - max=gdf['EndYear'].max(), + min=gdf['FromYear'].min(), + max=gdf['ToYear'].max(), description='Year:', ) # Link the text box and the slider widgets.jslink((year_input, 'value'), (year_slider, 'value')) - # Create an output widget - map_output = widgets.Output() + # Create a radio button to switch between "Polities" and "Components". + # The value should be a boolean indicating whether to display components or not. + components_radio = widgets.RadioButtons( + options=['Polities', 'Components'], + description='Display:', + disabled=False + ) + + # Define a function to be called when the value of the text box changes + def on_value_change(change): + """ + This function is called when the value of the text box or slider changes. + It calls create_map with the newly selected year and the GeoDataFrame gdf. + It sets the components parameter based on the current value of the radio button. + + Args: + change (dict): A dictionary containing information about the change. + + Returns: + None + """ + if components_radio.value == 'Polities': + create_map(change['new'], gdf, map_output) + elif components_radio.value == 'Components': + create_map(change['new'], gdf, map_output, components=True) + + # Define a function to be called when the value of the radio button changes + def on_radio_change(change): + """ + This function is called when the value of the radio button changes. It calls + create_map with the newly selected year of the text box and the GeoDataFrame gdf. + It sets the components parameter based on the current value of the radio button. + + Args: + change (dict): A dictionary containing information about the change. + + Returns: + None + """ + if change['new'] == 'Polities': + create_map(year_input.value, gdf, map_output) + elif change['new'] == 'Components': + create_map(year_input.value, gdf, map_output, components=True) # Attach the function to the text box year_input.observe(on_value_change, names='value') + # Attach the function to the radio button + components_radio.observe(on_radio_change, names='value') + + # Create an output widget + map_output = widgets.Output() + # Display the widgets - display(year_input, year_slider, map_output) + display(year_input, year_slider, components_radio, map_output) # Call create_map initially to display the map - create_map(display_year, gdf, map_output) \ No newline at end of file + create_map(display_year, gdf, map_output, ) \ No newline at end of file diff --git a/notebooks/requirements.txt b/notebooks/requirements.txt index 03f14b0c2..9ca63c8a1 100644 --- a/notebooks/requirements.txt +++ b/notebooks/requirements.txt @@ -2,4 +2,5 @@ jupyter==1.0.0 ipykernel==6.29.3 geopandas==0.13.2 contextily==1.6.0 -folium==0.16.0 \ No newline at end of file +folium==0.16.0 +distinctipy==1.2.3 \ No newline at end of file diff --git a/seshat/apps/core/management/commands/populate_videodata.py b/seshat/apps/core/management/commands/populate_videodata.py index 495333312..9a7754399 100644 --- a/seshat/apps/core/management/commands/populate_videodata.py +++ b/seshat/apps/core/management/commands/populate_videodata.py @@ -1,166 +1,67 @@ import os import json -import fnmatch -from distinctipy import get_colors, get_hex from django.contrib.gis.geos import GEOSGeometry, MultiPolygon from django.core.management.base import BaseCommand from django.db import connection from seshat.apps.core.models import VideoShapefile + class Command(BaseCommand): help = 'Populates the database with Shapefiles' def add_arguments(self, parser): - parser.add_argument('dir', type=str, help='Directory containing geojson files') + parser.add_argument('geojson_file', type=str, help='Path to the geojson file') def handle(self, *args, **options): - dir = options['dir'] + + # Ensure the file exists and has the suffix "_seshat_processed.geojson" + cliopatria_geojson_path = options['geojson_file'] + if not os.path.exists(cliopatria_geojson_path): + self.stdout.write(self.style.ERROR(f"File {cliopatria_geojson_path} does not exist")) + return + if not cliopatria_geojson_path.endswith('_seshat_processed.geojson'): + self.stdout.write(self.style.ERROR(f"File {cliopatria_geojson_path} should have the suffix '_seshat_processed.geojson'")) + self.stdout.write(self.style.ERROR(f"Please run the cliopatria/convert_data.py script first")) + return + + # Load the Cliopatria shape dataset with JSON + self.stdout.write(self.style.SUCCESS(f"Loading Cliopatria shape dataset from {cliopatria_geojson_path}...")) + with open(cliopatria_geojson_path) as f: + cliopatria_data = json.load(f) + self.stdout.write(self.style.SUCCESS(f"Successfully loaded Cliopatria shape dataset from {cliopatria_geojson_path}")) # Clear the VideoShapefile table self.stdout.write(self.style.SUCCESS('Clearing VideoShapefile table...')) VideoShapefile.objects.all().delete() self.stdout.write(self.style.SUCCESS('VideoShapefile table cleared')) - # Get the start and end years for each shape - # Load a file with 'name_years.json' in the filename kept in the same dir as the geojson files. - # Loads a dict of polity names and their start and end years. - # The values are lists of the form [[first_start_year, first_end_year], [second_start_year, second_end_year], ...] - - # List all files in the directory - files = os.listdir(dir) - - # Find the first file that includes 'name_years.json' in the filename - name_years_file = next((f for f in files if fnmatch.fnmatch(f, '*name_years.json*')), None) - - if name_years_file: - name_years_path = os.path.join(dir, name_years_file) - with open(name_years_path, 'r') as f: - name_years = json.load(f) - else: - self.stdout.write(self.style.ERROR("No file found with 'name_years.json' in the filename")) - - # Dict of all the shape years for a given polity - polity_years = {} - # Set of all polities, for generating colour mapping - all_polities = set() - # Dict of all the polities found and the shapes they include - polity_shapes = {} - # Iterate over files in the directory - for filename in os.listdir(dir): - if filename.endswith('.geojson'): - file_path = os.path.join(dir, filename) - - # Read and parse the GeoJSON file - with open(file_path, 'r') as geojson_file: - geojson_data = json.load(geojson_file) - - # Extract data and create VideoShapefile instances - for feature in geojson_data['features']: - properties = feature['properties'] - polity_name = properties['Name'].replace('(', '').replace(')', '') # Remove spaces and brackets from name - polity_colour_key = polity_name - try: - # If a shape has components we'll load the components instead - # ... unless the components have their own components, then load the top level shape - # ... or the shape is a personal union, then load the personal union shape - if properties['Components']: - if ';' not in properties['SeshatID']: - if len(properties['Components']) > 0 and '(' not in properties['Components']: - polity_name = None - except KeyError: - pass - try: - if properties['Member_of']: - # If a shape is a component, get the parent polity to use as the polity_colour_key - if len(properties['Member_of']) > 0: - polity_colour_key = properties['Member_of'].replace('(', '').replace(')', '') - except KeyError: - pass - if polity_name: - if properties['Type'] != 'POLITY': - polity_name = properties['Type'] + ': ' + polity_name - if polity_name not in polity_years: - polity_years[polity_name] = [] - polity_years[polity_name].append(properties['Year']) - if polity_colour_key not in polity_shapes: - polity_shapes[polity_colour_key] = [] - polity_shapes[polity_colour_key].append(feature) - - all_polities.add(polity_colour_key) - - self.stdout.write(self.style.SUCCESS(f'Found shape for {polity_name} ({properties["Year"]})')) - - # Sort the polities and generate a colour mapping - unique_polities = sorted(all_polities) - self.stdout.write(self.style.SUCCESS(f'Generating colour mapping for {len(unique_polities)} polities')) - pol_col_map = polity_colour_mapping(unique_polities) - self.stdout.write(self.style.SUCCESS(f'Colour mapping generated')) - - # Iterate through polity_shapes and create VideoShapefile instances - for polity_colour_key, features in polity_shapes.items(): - for feature in features: - properties = feature['properties'] - polity_name = properties["Name"].replace('(', '').replace(')', '') - if properties['Type'] != 'POLITY': - polity_name = properties['Type'] + ': ' + polity_name - self.stdout.write(self.style.SUCCESS(f'Importing shape for {polity_name} ({properties["Year"]})')) - - # Get a sorted list of the shape years this polity - this_polity_years = sorted(polity_years[polity_name]) - - # Get the end year for a shape - # Most of the time, the shape end year is the year of the next shape - # Some polities have a gap in their active years - # For a shape year at the start of a gap, set the end year to be the shape year, so it doesn't cover the inactive period - start_end_years = name_years[properties['Name']] - end_years = [x[1] for x in start_end_years] - - polity_start_year = start_end_years[0][0] - polity_end_year = end_years[-1] - - # Raise an error if the shape year is not the start year of the polity - if this_polity_years[0] != polity_start_year: - raise ValueError(f'First shape year for {polity_name} is not the start year of the polity') - - # Find the closest higher value from end_years to the shape year - next_end_year = min(end_years, key=lambda x: x if x >= properties['Year'] else float('inf')) - - if properties['Year'] in end_years: # If the shape year is in the list of polity end years, the start year is the end year - end_year = properties['Year'] - else: - this_year_index = this_polity_years.index(properties['Year']) - try: # Try to use the next shape year minus one as the end year if possible, unless it's higher than the next_end_year - next_shape_year_minus_one = this_polity_years[this_year_index + 1] - 1 - end_year = next_shape_year_minus_one if next_shape_year_minus_one < next_end_year else next_end_year - except IndexError: # Otherwise assume the end year of the shape is the end year of the polity - end_year = polity_end_year - - # Save geom and convert Polygon to MultiPolygon if necessary - geom = GEOSGeometry(json.dumps(feature['geometry'])) - if geom.geom_type == 'Polygon': - geom = MultiPolygon(geom) - - self.stdout.write(self.style.SUCCESS(f'Creating VideoShapefile instance for {polity_name} ({properties["Year"]} - {end_year})')) - - VideoShapefile.objects.create( - geom=geom, - name=polity_name, - polity=polity_colour_key, - wikipedia_name=properties['Wikipedia'], - seshat_id=properties['SeshatID'], - area=properties['Area'], - start_year=properties['Year'], - end_year=end_year, - polity_start_year=polity_start_year, - polity_end_year=polity_end_year, - colour=pol_col_map[polity_colour_key] - ) - - self.stdout.write(self.style.SUCCESS(f'Successfully imported shape for {polity_name} ({properties["Year"]})')) - - self.stdout.write(self.style.SUCCESS(f'Successfully imported all shapes for {polity_name}')) - - self.stdout.write(self.style.SUCCESS(f'Successfully imported all data from {filename}')) + # Iterate through the data and create VideoShapefile instances + self.stdout.write(self.style.SUCCESS('Adding data to the database...')) + for feature in cliopatria_data['features']: + properties = feature['properties'] + self.stdout.write(self.style.SUCCESS(f"Creating VideoShapefile instance for {properties['DisplayName']} ({properties['FromYear']} - {properties['ToYear']})")) + + # Save geom and convert Polygon to MultiPolygon if necessary + geom = GEOSGeometry(json.dumps(feature['geometry'])) + if geom.geom_type == 'Polygon': + geom = MultiPolygon(geom) + + VideoShapefile.objects.create( + geom=geom, + name=properties['DisplayName'], + wikipedia_name=properties['Wikipedia'], + seshat_id=properties['SeshatID'], + area=properties['Area'], + start_year=properties['FromYear'], + end_year=properties['ToYear'], + polity_start_year=properties['PolityStartYear'], + polity_end_year=properties['PolityEndYear'], + colour=properties['Color'], + components=properties['Components'], + member_of=properties['MemberOf'] + ) + + self.stdout.write(self.style.SUCCESS(f"Successfully imported all data from {cliopatria_geojson_path}")) ########################################################### ### Adjust the tolerance param of ST_Simplify as needed ### @@ -183,10 +84,3 @@ def handle(self, *args, **options): """) self.stdout.write(self.style.SUCCESS('Simplified geometries added')) - -def polity_colour_mapping(polities): - """Use DistinctiPy package to assign a colour to each polity""" - colours = [] - for col in get_colors(len(polities)): - colours.append(get_hex(col)) - return dict(zip(polities, colours)) diff --git a/seshat/apps/core/migrations/0066_alter_country_options_alter_polity_options_and_more.py b/seshat/apps/core/migrations/0066_alter_country_options_alter_polity_options_and_more.py new file mode 100644 index 000000000..57c1a4b12 --- /dev/null +++ b/seshat/apps/core/migrations/0066_alter_country_options_alter_polity_options_and_more.py @@ -0,0 +1,35 @@ +# Generated by Django 4.0.3 on 2024-07-12 10:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0065_alter_videoshapefile_id'), + ] + + operations = [ + migrations.AlterModelOptions( + name='country', + options={'verbose_name': 'country', 'verbose_name_plural': 'countries'}, + ), + migrations.AlterModelOptions( + name='polity', + options={'ordering': ['long_name'], 'verbose_name': 'polity', 'verbose_name_plural': 'polities'}, + ), + migrations.RemoveField( + model_name='videoshapefile', + name='polity', + ), + migrations.AddField( + model_name='videoshapefile', + name='components', + field=models.CharField(max_length=500, null=True), + ), + migrations.AddField( + model_name='videoshapefile', + name='member_of', + field=models.CharField(max_length=500, null=True), + ), + ] diff --git a/seshat/apps/core/models.py b/seshat/apps/core/models.py index 5aa8cf3d9..4ba866758 100644 --- a/seshat/apps/core/models.py +++ b/seshat/apps/core/models.py @@ -1123,13 +1123,12 @@ def __str__(self) -> str: class VideoShapefile(models.Model): """ - Model representing a video shapefile. + Model representing Cliopatria polity borders dataset. """ id = models.AutoField(primary_key=True) geom = models.MultiPolygonField() simplified_geom = models.MultiPolygonField(null=True) name=models.CharField(max_length=100) - polity=models.CharField(max_length=100) wikipedia_name=models.CharField(max_length=100, null=True) seshat_id=models.CharField(max_length=100) area=models.FloatField() @@ -1138,6 +1137,8 @@ class VideoShapefile(models.Model): polity_start_year=models.IntegerField() polity_end_year=models.IntegerField() colour=models.CharField(max_length=7) + components=models.CharField(max_length=500, null=True) + member_of=models.CharField(max_length=500, null=True) def __str__(self): return "Name: %s" % self.name diff --git a/seshat/apps/core/static/core/js/map_functions.js b/seshat/apps/core/static/core/js/map_functions.js index 4a0a6e770..9006c2d57 100644 --- a/seshat/apps/core/static/core/js/map_functions.js +++ b/seshat/apps/core/static/core/js/map_functions.js @@ -218,6 +218,7 @@ function updateLegend() { var legendDiv = document.getElementById('variableLegend'); var selectedYear1 = document.getElementById('dateSlide').value; // Giving it the same name as a var used in the templated JS caused an error var selectedYearInt1 = parseInt(selectedYear1); + var displayComponent = document.getElementById('switchPolitiesComponents').value; // Clear the current legend legendDiv.innerHTML = ''; @@ -226,24 +227,17 @@ function updateLegend() { var addedPolities = []; var addedPolityNames = []; shapesData.forEach(function (shape) { - // If the polity shape is part of a personal union or meta-polity active in the selected year, don't add it to the legend - var ignore = false; - if (shape.union_name) { - if ((parseInt(shape.union_start_year) <= selectedYearInt1 && parseInt(shape.union_end_year) >= selectedYearInt1)) { - ignore = true; - }; - }; - if (!ignore) { - shape_name_col_dict = {}; - shape_name_col_dict['polity'] = shape.polity; - shape_name_col_dict['colour'] = shape.colour; - if (shape.weight > 0 && !addedPolityNames.includes(shape_name_col_dict['polity'])) { - // If the shape spans the selected year - if ((parseInt(shape.start_year) <= selectedYearInt1 && parseInt(shape.end_year) >= selectedYearInt1)) { - // Add the polity to the list of added polities - addedPolities.push(shape_name_col_dict); - addedPolityNames.push(shape_name_col_dict['polity']); - }; + shape_name_col_dict = {}; + shape_name_col_dict['polity'] = shape.name; + shape_name_col_dict['colour'] = shape.colour; + if (shape.weight > 0 && !addedPolityNames.includes(shape_name_col_dict['polity'])) { + // If the shape spans the selected year and should be displayed according to the shouldDisplayComponent() function + if ((parseInt(shape.start_year) <= selectedYearInt1 && parseInt(shape.end_year) >= selectedYearInt1) + && shouldDisplayComponent(displayComponent, shape) + ) { + // Add the polity to the list of added polities + addedPolities.push(shape_name_col_dict); + addedPolityNames.push(shape_name_col_dict['polity']); }; }; }); @@ -402,4 +396,17 @@ function longAbsentPresentVarName(var_name){ var_name = `${var_name[0].toUpperCase()}${var_name.slice(1)}`; } return var_name; +} + +function shouldDisplayComponent(displayComponent, shape) { + if (displayComponent == 'polities' + && shape.seshat_id.includes(';') === false + && (shape.member_of === null || shape.member_of === '')) { + return true; + } else if (displayComponent == 'components' + && (shape.components === null || shape.components === '')) { + return true; + } else { + return false; + } } \ No newline at end of file diff --git a/seshat/apps/core/templates/core/polity_map.html b/seshat/apps/core/templates/core/polity_map.html index 0497730b8..360da9d97 100644 --- a/seshat/apps/core/templates/core/polity_map.html +++ b/seshat/apps/core/templates/core/polity_map.html @@ -21,8 +21,8 @@
-
-

+
+


@@ -34,6 +34,11 @@


+ +
@@ -168,13 +173,16 @@

// Convert to int, because for some reason JS converts it to a string var selectedYearInt = parseInt(selectedYear); + displayComponent = document.getElementById('switchPolitiesComponents').value; // Add shapes to the map // Don't plot them if "Base map only" checkbox selected if (!document.getElementById('baseMapOnly').checked) { polityMapShapesData.forEach(function (shape) { - // If the shape spans the selected year - if ((parseInt(shape.start_year) <= selectedYearInt && parseInt(shape.end_year) >= selectedYearInt)) { + // If the shape spans the selected year and should be displayed according to the shouldDisplayComponent() function + if ((parseInt(shape.start_year) <= selectedYearInt && parseInt(shape.end_year) >= selectedYearInt) + && shouldDisplayComponent(displayComponent, shape) + ) { // Format the area float const formattedArea = parseFloat(shape.area).toLocaleString('en-US', { @@ -210,18 +218,10 @@

var popupContent = ` - + `; - if (shape.polity != shape.name) { - popupContent = popupContent + ` - - - - - `; - } popupContent = popupContent + ` diff --git a/seshat/apps/core/templates/core/world_map.html b/seshat/apps/core/templates/core/world_map.html index 99d63d9a3..933bec489 100644 --- a/seshat/apps/core/templates/core/world_map.html +++ b/seshat/apps/core/templates/core/world_map.html @@ -187,10 +187,18 @@

Seshat World Map



+ +

+
+ +
-

+

@@ -513,6 +521,13 @@

document.getElementById('chooseCategoricalVariableSelectionFieldset').style.display = 'none'; } + // Show the switchPolitiesComponents dropdown if the variable is polity + if (variable == 'polity') { + document.getElementById('switchPolitiesComponentsFieldset').style.display = 'block'; + } else { + document.getElementById('switchPolitiesComponentsFieldset').style.display = 'none'; + } + updateLegend(); // Load capital info if not already loaded @@ -531,21 +546,7 @@

var selectedYearInt = parseInt(selectedYear); // Add shapes to the map // Don't plot them if "Base map only" checkbox selected - var dontPlotMeBecauseImInAUnion = []; if (!document.getElementById('baseMapOnly').checked){ - - // if there is a ';' in the seshat_id, it is inside a personal union or meta-polity, don't plot it when we are plotting the union - // so split the seshat_id by ';' and add each to the dontPlotMeBecauseImInAUnion list, when the year is inside the union period - if (variable == 'polity') { - shapesData.forEach(function (shape) { - if (shape.seshat_id.includes(';') && (parseInt(shape.start_year) <= selectedYearInt && parseInt(shape.end_year) >= selectedYearInt)) { - var these_seshat_ids = shape.seshat_id.split(';'); - these_seshat_ids.forEach(function (this_seshat_id) { - dontPlotMeBecauseImInAUnion.push(this_seshat_id); - }); - }; - }); - }; // Iterate through shapesData and plot the shapes shapesData.forEach(function (shape) { @@ -620,9 +621,12 @@

} } - // If the shape spans the selected year - // Ignore shapes that are in a union during this year (because we are plotting the union) - if ((parseInt(shape.start_year) <= selectedYearInt && parseInt(shape.end_year) >= selectedYearInt) && !dontPlotMeBecauseImInAUnion.includes(shape.seshat_id)) { + displayComponent = document.getElementById('switchPolitiesComponents').value; + + // If the shape spans the selected year and should be displayed according to the shouldDisplayComponent() function + if ((parseInt(shape.start_year) <= selectedYearInt && parseInt(shape.end_year) >= selectedYearInt) + && shouldDisplayComponent(displayComponent, shape) + ) { if (shapeWeight > 0){ atLeastOnePolityHighlighted = true; @@ -673,25 +677,17 @@

// Add seshat_id to the layer and add a blank string if it doesn't exist layer.feature.properties.seshat_id = shape.seshat_id || ''; // Add polity name to the layer - layer.feature.properties.polity = shape.polity; + layer.feature.properties.name = shape.name; // Add the colour to the layer layer.feature.properties.colour = shapeColour; var popupContent = `
${shape.polity}${shape.name}
Component${shape.name}
Duration
- + `; - if (shape.polity != shape.name) { - popupContent = popupContent + ` - - - - - `; - } if (shape.seshat_id in seshat_id_page_id) { var polityId = seshat_id_page_id[shape.seshat_id]['id']; longName = seshat_id_page_id[shape.seshat_id]['long_name']; @@ -846,7 +842,7 @@

}; // Update weights in this layer and others of the same polity map.eachLayer(function (layer) { - if (layer.feature && layer.feature.properties && layer.feature.properties.polity === shape.polity ) { + if (layer.feature && layer.feature.properties && layer.feature.properties.name === shape.name ) { layer.setStyle({ weight: newWeight }); @@ -855,7 +851,7 @@

// Update weights in shapesData var i = 0; shapesData.forEach(function (shape2) { - if (shape2.polity === shape.polity) { + if (shape2.name === shape.name) { shapesData[i]['weight'] = newWeight; } // Set the weight for shapes with multiple seshat_ids e.g. personal unions TODO: fixme (this results in lots of things getting their weight updated) diff --git a/seshat/apps/core/tests/tests.py b/seshat/apps/core/tests/tests.py index c8ffc415b..b12be7788 100644 --- a/seshat/apps/core/tests/tests.py +++ b/seshat/apps/core/tests/tests.py @@ -48,29 +48,31 @@ def setUp(self): id=1, geom=self.square, simplified_geom=self.square, - name="Test shape", - polity="Testpolityname", + name="Testpolityname", seshat_id="Test seshat_id", area=100.0, start_year=2000, end_year=2020, polity_start_year=2000, polity_end_year=2020, - colour="#FFFFFF" + colour="#FFFFFF", + components="Test components", + member_of="Test member_of" ) VideoShapefile.objects.create( id=2, geom=self.square, simplified_geom=self.square, - name="Test shape 2", - polity="Testpolityname2", + name="Testpolityname2", seshat_id="Test seshat_id 2", area=100.0, start_year=0, end_year=1000, polity_start_year=0, polity_end_year=1000, - colour="#FFFFFF" + colour="#FFFFFF", + components="Test components", + member_of="Test member_of" ) self.gadm_shapefile = GADMShapefile.objects.create( geom=self.square, @@ -160,7 +162,7 @@ def setUp(self): def test_video_shapefile_creation(self): """Test the creation of a VideoShapefile instance.""" self.assertIsInstance(self.video_shapefile, VideoShapefile) - self.assertEqual(self.video_shapefile.name, "Test shape") + self.assertEqual(self.video_shapefile.name, "Testpolityname") def test_gadm_shapefile_creation(self): """Test the creation of a GADMShapefile instance.""" @@ -200,8 +202,7 @@ def test_get_polity_shape_content(self): 'shapes': [ { 'seshat_id': 'Test seshat_id', - 'name': 'Test shape', - 'polity': 'Testpolityname', + 'name': 'Testpolityname', 'start_year': 2000, 'end_year': 2020, 'polity_start_year': 2000, @@ -209,12 +210,13 @@ def test_get_polity_shape_content(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 1 + 'id': 1, + 'components': 'Test components', + 'member_of': 'Test member_of', }, { 'seshat_id': 'Test seshat_id 2', - 'name': 'Test shape 2', - 'polity': 'Testpolityname2', + 'name': 'Testpolityname2', 'start_year': 0, 'end_year': 1000, 'polity_start_year': 0, @@ -222,7 +224,9 @@ def test_get_polity_shape_content(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 2 + 'id': 2, + 'components': 'Test components', + 'member_of': 'Test member_of', } ], 'earliest_year': 0, @@ -247,8 +251,7 @@ def test_get_polity_shape_content_single_year(self): 'shapes': [ { 'seshat_id': 'Test seshat_id', - 'name': 'Test shape', - 'polity': 'Testpolityname', + 'name': 'Testpolityname', 'start_year': 2000, 'end_year': 2020, 'polity_start_year': 2000, @@ -256,7 +259,9 @@ def test_get_polity_shape_content_single_year(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 1 + 'id': 1, + 'components': 'Test components', + 'member_of': 'Test member_of', } ], 'earliest_year': 0, # This is the earliest year in the database, not the earliest year of the polity @@ -280,8 +285,7 @@ def test_get_polity_shape_content_single_seshat_id(self): 'shapes': [ { 'seshat_id': 'Test seshat_id', - 'name': 'Test shape', - 'polity': 'Testpolityname', + 'name': 'Testpolityname', 'start_year': 2000, 'end_year': 2020, 'polity_start_year': 2000, @@ -289,7 +293,9 @@ def test_get_polity_shape_content_single_seshat_id(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 1 + 'id': 1, + 'components': 'Test components', + 'member_of': 'Test member_of', } ], 'earliest_year': 2000, # This is the earliest year of the polity @@ -336,8 +342,7 @@ def test_polity_map(self): 'shapes': [ { 'seshat_id': 'Test seshat_id', - 'name': 'Test shape', - 'polity': 'Testpolityname', + 'name': 'Testpolityname', 'start_year': 2000, 'end_year': 2020, 'polity_start_year': 2000, @@ -345,7 +350,9 @@ def test_polity_map(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 1 + 'id': 1, + 'components': 'Test components', + 'member_of': 'Test member_of', } ], 'earliest_year': 2000, @@ -372,8 +379,7 @@ def test_polity_map_no_peak_year_set(self): 'shapes': [ { 'seshat_id': 'Test seshat_id 2', - 'name': 'Test shape 2', - 'polity': 'Testpolityname2', + 'name': 'Testpolityname2', 'start_year': 0, 'end_year': 1000, 'polity_start_year': 0, # Note: this is taken from the shape objectm, not the polity object (they don't match in this test case) @@ -381,7 +387,9 @@ def test_polity_map_no_peak_year_set(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 2 + 'id': 2, + 'components': 'Test components', + 'member_of': 'Test member_of', } ], 'earliest_year': 0, @@ -433,8 +441,7 @@ def test_assign_variables_to_shapes(self): shapes = [ { 'seshat_id': 'Test seshat_id 2', - 'name': 'Test shape 2', - 'polity': 'Testpolityname2', + 'name': 'Testpolityname2', 'start_year': 0, 'end_year': 1000, 'polity_start_year': 0, @@ -442,7 +449,9 @@ def test_assign_variables_to_shapes(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 2 + 'id': 2, + 'components': 'Test components', + 'member_of': 'Test member_of', } ] app_map = { @@ -473,8 +482,7 @@ def test_assign_categorical_variables_to_shapes(self): shapes = [ { 'seshat_id': 'Test seshat_id 2', - 'name': 'Test shape 2', - 'polity': 'Testpolityname2', + 'name': 'Testpolityname2', 'start_year': 0, 'end_year': 1000, 'polity_start_year': 0, @@ -482,7 +490,9 @@ def test_assign_categorical_variables_to_shapes(self): 'colour': "#FFFFFF", 'area': 100.0, 'geom_json': self.geo_square, - 'id': 2 + 'id': 2, + 'components': 'Test components', + 'member_of': 'Test member_of', } ] result_shapes, result_variables = assign_categorical_variables_to_shapes(shapes, {}) diff --git a/seshat/apps/core/views.py b/seshat/apps/core/views.py index c69607935..5065157d0 100644 --- a/seshat/apps/core/views.py +++ b/seshat/apps/core/views.py @@ -3835,7 +3835,9 @@ def get_polity_shape_content(displayed_year="all", seshat_id="all", tick_number= # Convert 'geom' to GeoJSON in the database query - rows = rows.annotate(geom_json=AsGeoJSON('geom')).values('id', 'seshat_id', 'name', 'polity', 'start_year', 'end_year', 'polity_start_year', 'polity_end_year', 'colour', 'area', 'geom_json') + rows = rows.annotate(geom_json=AsGeoJSON('geom')) + # Filter the rows to return + rows = rows.values('id', 'seshat_id', 'name', 'start_year', 'end_year', 'polity_start_year', 'polity_end_year', 'colour', 'area', 'geom_json', 'components', 'member_of') shapes = list(rows)
${shape.polity}${shape.name}
Component${shape.name}