From 651374e7573d39bb87a7379b9fb6f2c5c2e93601 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 24 Jan 2023 15:15:37 +0200 Subject: [PATCH 001/188] Mock fetch_json function --- mobility_data/tests/test_import_foli_stops.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobility_data/tests/test_import_foli_stops.py b/mobility_data/tests/test_import_foli_stops.py index 78e91473a..f2d087c11 100644 --- a/mobility_data/tests/test_import_foli_stops.py +++ b/mobility_data/tests/test_import_foli_stops.py @@ -9,11 +9,11 @@ @pytest.mark.django_db -@patch("mobility_data.importers.foli_stops.get_json_data") -def test_import_foli_stops(get_json_data_mock): +@patch("mobility_data.importers.utils.fetch_json") +def test_import_foli_stops(fetch_json_mock): from mobility_data.importers import foli_stops - get_json_data_mock.return_value = get_json_data("foli_stops.json") + fetch_json_mock.return_value = get_json_data("foli_stops.json") objects = foli_stops.get_foli_stops() foli_stops.save_to_database(objects) assert ContentType.objects.count() == 1 From 071ccea30059bff608b5979f8b316208a9a4a433 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 24 Jan 2023 15:20:18 +0200 Subject: [PATCH 002/188] Use fetch_json function in utils --- mobility_data/importers/foli_stops.py | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/mobility_data/importers/foli_stops.py b/mobility_data/importers/foli_stops.py index d81639d13..8328bcc57 100644 --- a/mobility_data/importers/foli_stops.py +++ b/mobility_data/importers/foli_stops.py @@ -1,12 +1,11 @@ import logging -import requests from django import db from django.contrib.gis.geos import Point from mobility_data.models import MobileUnit -from .utils import delete_mobile_units, get_or_create_content_type +from .utils import delete_mobile_units, fetch_json, get_or_create_content_type URL = "http://data.foli.fi/gtfs/stops" CONTENT_TYPE_NAME = "FoliStop" @@ -26,18 +25,8 @@ def __init__(self, stop_data): self.extra["wheelchair_boarding"] = stop_data["wheelchair_boarding"] -def get_json_data(): - response = requests.get(URL) - assert ( - response.status_code == 200 - ), "Unable to fetch föli stops from url: {}, status code: {}".format( - URL, response.status_code - ) - return response.json() - - def get_foli_stops(): - json_data = get_json_data() + json_data = fetch_json(URL) objects = [] for stop_code in json_data: objects.append(FoliStop(json_data[stop_code])) @@ -45,7 +34,7 @@ def get_foli_stops(): @db.transaction.atomic -def create_foli_stop_content_type(): +def get_and_create_foli_stop_content_type(): description = "Föli stops." content_type, _ = get_or_create_content_type(CONTENT_TYPE_NAME, description) return content_type @@ -56,7 +45,7 @@ def save_to_database(objects, delete_tables=True): if delete_tables: delete_mobile_units(CONTENT_TYPE_NAME) - content_type = create_foli_stop_content_type() + content_type = get_and_create_foli_stop_content_type() for object in objects: MobileUnit.objects.create( content_type=content_type, From 780ded2996e0245f1004a274bb9165b98660a5c4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 24 Jan 2023 15:31:05 +0200 Subject: [PATCH 003/188] =?UTF-8?q?Add=20importer=20for=20F=C3=B6li=20park?= =?UTF-8?q?=20and=20ride=20stops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../importers/foli_parkandride_stop.py | 106 ++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 mobility_data/importers/foli_parkandride_stop.py diff --git a/mobility_data/importers/foli_parkandride_stop.py b/mobility_data/importers/foli_parkandride_stop.py new file mode 100644 index 000000000..0c1df074d --- /dev/null +++ b/mobility_data/importers/foli_parkandride_stop.py @@ -0,0 +1,106 @@ +from django import db +from django.contrib.gis.geos import Point +from munigeo.models import Municipality + +from mobility_data.models import MobileUnit + +from .utils import ( + delete_mobile_units, + fetch_json, + get_or_create_content_type, + set_translated_field, +) + +URL = "https://data.foli.fi/geojson/poi" +FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME = "FoliParkAndRideCarsStop" +FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME = "FoliParkAndRideBikesStop" +PARKANDRIDE_CARS = "PARKANDRIDE_CARS" +PARKANDRIDE_BIKES = "PARKANDRIDE_BIKES" +SOURCE_DATA_SRID = 4326 + + +class ParkAndRideStop: + def __init__(self, feature): + properties = feature["properties"] + self.name = { + "fi": properties["name_fi"], + "sv": properties["name_sv"], + "en": properties["name_en"], + } + self.address = { + "fi": properties["address_fi"], + "sv": properties["address_sv"], + "en": properties["address_fi"], + } + self.address_zip = properties["text"].split(" ")[-1] + self.description = properties["text"] + try: + self.municipality = Municipality.objects.get(name=properties["city"]) + except Municipality.DoesNotExist: + self.municipality = None + + geometry = feature["geometry"] + self.geometry = Point( + geometry["coordinates"][0], + geometry["coordinates"][1], + srid=SOURCE_DATA_SRID, + ) + + +def get_objects(): + json_data = fetch_json(URL) + car_stops = [] + bike_stops = [] + for feature in json_data["features"]: + if feature["properties"]["category"] == PARKANDRIDE_CARS: + car_stops.append(ParkAndRideStop(feature)) + elif feature["properties"]["category"] == PARKANDRIDE_BIKES: + bike_stops.append(ParkAndRideStop(feature)) + + return car_stops, bike_stops + + +@db.transaction.atomic +def get_and_create_foli_parkandride_bike_stop_content_type(): + description = "Föli park and ride bike stop." + content_type, _ = get_or_create_content_type( + FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME, description + ) + return content_type + + +@db.transaction.atomic +def get_and_create_foli_parkandride_car_stop_content_type(): + description = "Föli park and ride car stop." + content_type, _ = get_or_create_content_type( + FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME, description + ) + return content_type + + +@db.transaction.atomic +def save_to_database(objects, content_type_name, delete_tables=True): + assert ( + content_type_name == FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME + or content_type_name == FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME + ) + if delete_tables: + delete_mobile_units(content_type_name) + + if content_type_name == FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME: + content_type = get_and_create_foli_parkandride_bike_stop_content_type() + elif content_type_name == FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME: + content_type = get_and_create_foli_parkandride_car_stop_content_type() + + for object in objects: + mobile_unit = MobileUnit.objects.create( + content_type=content_type, + geometry=object.geometry, + address_zip=object.address_zip, + description=object.description, + ) + set_translated_field(mobile_unit, "name", object.name) + set_translated_field(mobile_unit, "address", object.address) + mobile_unit.save() + + return len(objects) From 0decc0006b3d77eaa53df737fa18293b105df965 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 24 Jan 2023 15:32:05 +0200 Subject: [PATCH 004/188] Use BaseCommand as base class --- mobility_data/management/commands/import_foli_stops.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobility_data/management/commands/import_foli_stops.py b/mobility_data/management/commands/import_foli_stops.py index 21977c8d6..ebefee2d5 100644 --- a/mobility_data/management/commands/import_foli_stops.py +++ b/mobility_data/management/commands/import_foli_stops.py @@ -1,13 +1,13 @@ import logging -from mobility_data.importers.foli_stops import get_foli_stops, save_to_database +from django.core.management import BaseCommand -from ._base_import_command import BaseImportCommand +from mobility_data.importers.foli_stops import get_foli_stops, save_to_database logger = logging.getLogger("mobility_data") -class Command(BaseImportCommand): +class Command(BaseCommand): def handle(self, *args, **options): logger.info("Importing Föli stops") objects = get_foli_stops() From 3105a1598995baea08f71ccfae47720c2cc07384 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 24 Jan 2023 15:40:47 +0200 Subject: [PATCH 005/188] =?UTF-8?q?Add=20management=20command=20to=20impor?= =?UTF-8?q?t=20F=C3=B6li=20park=20and=20ride=20stops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../commands/import_foli_parkandride_stops.py | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 mobility_data/management/commands/import_foli_parkandride_stops.py diff --git a/mobility_data/management/commands/import_foli_parkandride_stops.py b/mobility_data/management/commands/import_foli_parkandride_stops.py new file mode 100644 index 000000000..bc71de8c7 --- /dev/null +++ b/mobility_data/management/commands/import_foli_parkandride_stops.py @@ -0,0 +1,25 @@ +import logging + +from django.core.management import BaseCommand + +from mobility_data.importers.foli_parkandride_stop import ( + FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME, + FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME, + get_objects, + save_to_database, +) + +logger = logging.getLogger("mobility_data") + + +class Command(BaseCommand): + def handle(self, *args, **options): + car_stops, bike_stops = get_objects() + logger.info( + f"Saved {save_to_database(car_stops, FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME)} " + "Föli park and ride car stops to database" + ) + logger.info( + f"Saved {save_to_database(bike_stops, FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME)} " + "Föli park and ride bike stops to database" + ) From 63ea39dc190c7a754a2f68baebe6ca7feb99d588 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 09:45:53 +0200 Subject: [PATCH 006/188] =?UTF-8?q?Add=20F=C3=B6li=20parkandride=20stops?= =?UTF-8?q?=20importer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobility_data/management/commands/import_mobility_data.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobility_data/management/commands/import_mobility_data.py b/mobility_data/management/commands/import_mobility_data.py index 8515c554d..c514b3dc8 100644 --- a/mobility_data/management/commands/import_mobility_data.py +++ b/mobility_data/management/commands/import_mobility_data.py @@ -1,5 +1,5 @@ """ -Main importer for mobility data sources. +Imports all mobility data sources. """ import logging @@ -24,6 +24,7 @@ "lounaistieto_shapefiles", "foli_stops", "outdoor_gym_devices", + "foli_parkandride_stops", ] # Read the content type names to be imported wfs_content_type_names = get_configured_cotent_type_names() From 608540093b9e64b6bc480c38e74af165426f3047 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:06:59 +0200 Subject: [PATCH 007/188] Rename get_json_data to get_test_fixture_json_data --- mobility_data/tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/tests/utils.py b/mobility_data/tests/utils.py index a159d2a83..89e3b8a24 100644 --- a/mobility_data/tests/utils.py +++ b/mobility_data/tests/utils.py @@ -20,7 +20,7 @@ def import_command(command, *args, **kwargs): ) -def get_json_data(file_name): +def get_test_fixture_json_data(file_name): data_path = os.path.join(os.path.dirname(__file__), "data") file = os.path.join(data_path, file_name) with open(file) as f: From 13a7b8db564b4c2f985c9ff0d394c9934c525b94 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:07:38 +0200 Subject: [PATCH 008/188] =?UTF-8?q?Add=20task=20to=20import=20F=C3=B6li=20?= =?UTF-8?q?parkandride=20stops?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mobility_data/tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/tasks.py b/mobility_data/tasks.py index 3e2ab1994..f5243c6a3 100644 --- a/mobility_data/tasks.py +++ b/mobility_data/tasks.py @@ -63,6 +63,11 @@ def import_foli_stops(name="import_foli_stops"): management.call_command("import_foli_stops") +@shared_task +def import_foli_parkandride_stops(name="import_foli_parkandride_stops"): + management.call_command("import_foli_parkandride_stops") + + @shared_task def import_outdoor_gym_devices(name="import_outdoor_gym_devices"): management.call_command("import_outdoor_gym_devices") From 8cda0410e99dd29fcbdc6340ab47e509cbb25fd9 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:10:24 +0200 Subject: [PATCH 009/188] Set DEFAULT_SRID to geometry --- mobility_data/importers/foli_stops.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mobility_data/importers/foli_stops.py b/mobility_data/importers/foli_stops.py index 8328bcc57..652d850f1 100644 --- a/mobility_data/importers/foli_stops.py +++ b/mobility_data/importers/foli_stops.py @@ -1,6 +1,7 @@ import logging from django import db +from django.conf import settings from django.contrib.gis.geos import Point from mobility_data.models import MobileUnit @@ -21,6 +22,7 @@ def __init__(self, stop_data): lon = stop_data["stop_lon"] lat = stop_data["stop_lat"] self.geometry = Point(lon, lat, srid=SOURCE_DATA_SRID) + self.geometry.transform(settings.DEFAULT_SRID) self.extra["stop_code"] = stop_data["stop_code"] self.extra["wheelchair_boarding"] = stop_data["wheelchair_boarding"] From 4fc0ed24f5c90db5d0ac1aa649f532f8a375b585 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:48:03 +0200 Subject: [PATCH 010/188] Add info about importing park and ride stops --- mobility_data/README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mobility_data/README.md b/mobility_data/README.md index 4a64680e3..338a959ce 100644 --- a/mobility_data/README.md +++ b/mobility_data/README.md @@ -159,7 +159,11 @@ To import data type: ``` ./manage.py import_foli_stops ``` - +### Föli park and ride stop +Imports park and ride stops for bikes and cars. +``` +./manage.py import_foli_parkandride_stops +``` ### Outdoor gym devices Imports the outdoor gym devices from the services.unit model. i.e., sets references by id to the services.unit model. The data is then serialized from the services.unit model. ``` From 41ebb562e8aaf7ffd9db923f1f14f3fea5177858 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:48:46 +0200 Subject: [PATCH 011/188] Set default srid --- mobility_data/importers/foli_parkandride_stop.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobility_data/importers/foli_parkandride_stop.py b/mobility_data/importers/foli_parkandride_stop.py index 0c1df074d..048b3d4e4 100644 --- a/mobility_data/importers/foli_parkandride_stop.py +++ b/mobility_data/importers/foli_parkandride_stop.py @@ -1,4 +1,5 @@ from django import db +from django.conf import settings from django.contrib.gis.geos import Point from munigeo.models import Municipality @@ -38,16 +39,16 @@ def __init__(self, feature): self.municipality = Municipality.objects.get(name=properties["city"]) except Municipality.DoesNotExist: self.municipality = None - geometry = feature["geometry"] self.geometry = Point( geometry["coordinates"][0], geometry["coordinates"][1], srid=SOURCE_DATA_SRID, ) + self.geometry.transform(settings.DEFAULT_SRID) -def get_objects(): +def get_parkandride_stop_objects(): json_data = fetch_json(URL) car_stops = [] bike_stops = [] @@ -98,6 +99,7 @@ def save_to_database(objects, content_type_name, delete_tables=True): geometry=object.geometry, address_zip=object.address_zip, description=object.description, + municipality=object.municipality, ) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) From d73235f2d381ff159738561393232d911c522181 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:49:24 +0200 Subject: [PATCH 012/188] Use get_parkandride_stop_objects function --- .../management/commands/import_foli_parkandride_stops.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobility_data/management/commands/import_foli_parkandride_stops.py b/mobility_data/management/commands/import_foli_parkandride_stops.py index bc71de8c7..f237fd912 100644 --- a/mobility_data/management/commands/import_foli_parkandride_stops.py +++ b/mobility_data/management/commands/import_foli_parkandride_stops.py @@ -5,7 +5,7 @@ from mobility_data.importers.foli_parkandride_stop import ( FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME, FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME, - get_objects, + get_parkandride_stop_objects, save_to_database, ) @@ -14,7 +14,7 @@ class Command(BaseCommand): def handle(self, *args, **options): - car_stops, bike_stops = get_objects() + car_stops, bike_stops = get_parkandride_stop_objects() logger.info( f"Saved {save_to_database(car_stops, FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME)} " "Föli park and ride car stops to database" From 2ea516362eec4582d6f945c6a6d3af469d0df5a3 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:50:08 +0200 Subject: [PATCH 013/188] Rename municipality fixture to municipalities, add Raisio and Lieto --- mobility_data/tests/conftest.py | 26 +++++++++++++++----------- 1 file changed, 15 insertions(+), 11 deletions(-) diff --git a/mobility_data/tests/conftest.py b/mobility_data/tests/conftest.py index 7f91edc86..712225228 100644 --- a/mobility_data/tests/conftest.py +++ b/mobility_data/tests/conftest.py @@ -89,9 +89,12 @@ def mobile_unit_group(group_type): @pytest.mark.django_db @pytest.fixture -def municipality(): - muni = Municipality.objects.create(id="turku", name="Turku") - return muni +def municipalities(): + munis = [] + munis.append(Municipality.objects.create(id="turku", name="Turku")) + munis.append(Municipality.objects.create(id="lieto", name="Lieto")) + munis.append(Municipality.objects.create(id="raisio", name="Raisio")) + return munis @pytest.mark.django_db @@ -180,11 +183,12 @@ def streets(): @pytest.mark.django_db @pytest.fixture -def address(streets, municipality): +def address(streets, municipalities): + turku_muni = municipalities[0] addresses = [] location = Point(22.244, 60.4, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=100, location=location, street=streets[0], @@ -195,7 +199,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.227168, 60.4350612, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=101, location=location, street=streets[1], @@ -205,7 +209,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.264457, 60.448905, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=102, location=location, street=streets[2], @@ -216,7 +220,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.2383, 60.411726, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=103, location=location, street=streets[3], @@ -227,7 +231,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.2871092678621, 60.44677715747775, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=104, location=location, street=streets[4], @@ -238,7 +242,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.26097246971352, 60.45055294118857, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=105, location=location, street=streets[5], @@ -249,7 +253,7 @@ def address(streets, municipality): addresses.append(address) location = Point(22.247047171564706, 60.45159033848499, srid=4326) address = Address.objects.create( - municipality_id=municipality.id, + municipality_id=turku_muni.id, id=106, location=location, street=streets[6], From b6b522074b32bb2ae777850f1af6782a7f2ca0e4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:51:33 +0200 Subject: [PATCH 014/188] Change municiaplity fixture to municipalities --- mobility_data/tests/test_import_bicycle_stands.py | 4 ++-- mobility_data/tests/test_import_charging_stations.py | 2 +- .../tests/test_import_disabled_and_no_staff_parkings.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/mobility_data/tests/test_import_bicycle_stands.py b/mobility_data/tests/test_import_bicycle_stands.py index d14fa0477..9d85ba92a 100644 --- a/mobility_data/tests/test_import_bicycle_stands.py +++ b/mobility_data/tests/test_import_bicycle_stands.py @@ -7,7 +7,7 @@ @pytest.mark.django_db def test_geojson_import( - municipality, + municipalities, administrative_division_type, administrative_division, administrative_division_geometry, @@ -38,7 +38,7 @@ def test_geojson_import( @pytest.mark.django_db def test_wfs_importer( - municipality, + municipalities, administrative_division_type, administrative_division, administrative_division_geometry, diff --git a/mobility_data/tests/test_import_charging_stations.py b/mobility_data/tests/test_import_charging_stations.py index 233649e49..97572225f 100644 --- a/mobility_data/tests/test_import_charging_stations.py +++ b/mobility_data/tests/test_import_charging_stations.py @@ -10,7 +10,7 @@ @pytest.mark.django_db def test_import_charging_stations( - municipality, + municipalities, administrative_division_type, administrative_division, administrative_division_geometry, diff --git a/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py b/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py index 0bc14d531..86226e4e0 100644 --- a/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py +++ b/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py @@ -11,7 +11,7 @@ @pytest.mark.django_db -def test_geojson_import(municipality): +def test_geojson_import(municipalities): import_command( "import_disabled_and_no_staff_parkings", test_mode="autopysäköinti_eihlö.geojson", From 84543a98f3313a0ef92b2fbbe545a8f36459f787 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 13:53:04 +0200 Subject: [PATCH 015/188] Add fixture data to park and ride stops importer tests --- .../tests/data/foli_parkandride_stops.json | 123 ++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 mobility_data/tests/data/foli_parkandride_stops.json diff --git a/mobility_data/tests/data/foli_parkandride_stops.json b/mobility_data/tests/data/foli_parkandride_stops.json new file mode 100644 index 000000000..0cf593528 --- /dev/null +++ b/mobility_data/tests/data/foli_parkandride_stops.json @@ -0,0 +1,123 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "id": "poi_1054", + "geometry": { + "type": "Point", + "coordinates": [ + 22.4579, + 60.50413 + ] + }, + "properties": { + "category": "PARKANDRIDE_CARS", + "name": "Lieto Keskusta, K-Supermarket Lietorin piha", + "name_fi": "Lieto Keskusta, K-Supermarket Lietorin piha", + "name_sv": "Lundo centrum, K-Supermarket Lietori", + "name_en": "Lieto centre, K-Supermarket Lietori", + "popup": "
\n Lieto Keskusta, K-Supermarket Lietorin piha<\/span>\n
Hyv\u00e4ttyl\u00e4ntie 2<\/div>\n<\/div>\n", + "text": "Lieto Keskusta, K-Supermarket Lietorin piha\nHyv\u00e4ttyl\u00e4ntie 2, 21420", + "city": "Lieto", + "city_fi": "Lieto", + "city_sv": "Lundo", + "address": "Hyv\u00e4ttyl\u00e4ntie 2", + "address_fi": "Hyv\u00e4ttyl\u00e4ntie 2", + "address_sv": "Hyv\u00e4ttyl\u00e4ntie 2", + "icon": { + "id": "icon_parkandride_cars", + "svg": "\n \n \n \n \n \n <\/g>\n<\/svg>\n" + } + } + }, + { + "type": "Feature", + "id": "poi_1057", + "geometry": { + "type": "Point", + "coordinates": [ + 22.3808, + 60.50614 + ] + }, + "properties": { + "category": "PARKANDRIDE_CARS", + "name": "Lieto Ilmarinen K-Market Ilmarisen piha", + "name_fi": "Lieto Ilmarinen K-Market Ilmarisen piha", + "name_sv": "Lundo Ilmarinen, K-Market Ilmarinen", + "name_en": "Lieto Ilmarinen, K-Market Ilmarinen", + "popup": "
\n Lieto Ilmarinen K-Market Ilmarisen piha<\/span>\n
Vanha Tampereentie 868<\/div>\n<\/div>\n", + "text": "Lieto Ilmarinen K-Market Ilmarisen piha\nVanha Tampereentie 868, 21350", + "city": "Lieto", + "city_fi": "Lieto", + "city_sv": "Lundo", + "address": "Vanha Tampereentie 868", + "address_fi": "Vanha Tampereentie 868", + "address_sv": "Vanha Tampereentie 868", + "icon": { + "id": "icon_parkandride_cars" + } + } + }, + { + "type": "Feature", + "id": "poi_1059", + "geometry": { + "type": "Point", + "coordinates": [ + 22.17121, + 60.48494 + ] + }, + "properties": { + "category": "PARKANDRIDE_BIKES", + "name": "St1 Raisio", + "name_fi": "St1 Raisio", + "name_sv": "St1 Raisio", + "name_en": "St1 Raisio", + "popup": "
\n St1 Raisio<\/span>\n
Kirkkov\u00e4\u00e4rtinkuja 2<\/div>\n<\/div>\n", + "text": "St1 Raisio\nKirkkov\u00e4\u00e4rtinkuja 2, 21200", + "city": "Raisio", + "city_fi": "Raisio", + "city_sv": "Reso", + "address": "Kirkkov\u00e4\u00e4rtinkuja 2", + "address_fi": "Kirkkov\u00e4\u00e4rtinkuja 2", + "address_sv": "Kirkkov\u00e4\u00e4rtinkuja 2", + "icon": { + "id": "icon_parkandride_bikes", + "svg": "\n \n \n \n \n \n \n \n <\/g>\n<\/svg>\n" + } + } + }, + { + "type": "Feature", + "id": "poi_1083", + "geometry": { + "type": "Point", + "coordinates": [ + 22.293, + 60.43613 + ] + }, + "properties": { + "category": "PARKANDRIDE_BIKES", + "name": "Kurjenm\u00e4ki", + "name_fi": "Kurjenm\u00e4ki", + "name_sv": "Kurjenm\u00e4ki", + "name_en": "Kurjenm\u00e4ki", + "popup": "
\n Kurjenm\u00e4ki<\/span>\n
Uudenmaankatu<\/div>\n<\/div>\n", + "text": "Kurjenm\u00e4ki\nUudenmaankatu, 20720", + "city": "Turku", + "city_fi": "Turku", + "city_sv": "\u00c5bo", + "address": "Uudenmaankatu", + "address_fi": "Uudenmaankatu", + "address_sv": "Uudenmaankatu", + "icon": { + "id": "icon_parkandride_bikes" + } + } + } + ] +} \ No newline at end of file From d4c013c9e460355dcc3211fbf001b6154898d8d4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 14:05:36 +0200 Subject: [PATCH 016/188] Use municipalities fixture --- mobility_data/tests/test_import_loading_and_unloading_places.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/tests/test_import_loading_and_unloading_places.py b/mobility_data/tests/test_import_loading_and_unloading_places.py index 42a344f22..2406df705 100644 --- a/mobility_data/tests/test_import_loading_and_unloading_places.py +++ b/mobility_data/tests/test_import_loading_and_unloading_places.py @@ -9,7 +9,7 @@ @pytest.mark.django_db @pytest.mark.django_db -def test_import(municipality): +def test_import(municipalities): import_command( "import_loading_and_unloading_places", test_mode="loading_and_unloading_places.geojson", From 2d04f60fdb165dac410ba02b2fb12b34a9745c3e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 14:06:34 +0200 Subject: [PATCH 017/188] Replace get_json_data to get_test_fixture_json_data --- mobility_data/tests/test_import_foli_stops.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mobility_data/tests/test_import_foli_stops.py b/mobility_data/tests/test_import_foli_stops.py index f2d087c11..e940984e9 100644 --- a/mobility_data/tests/test_import_foli_stops.py +++ b/mobility_data/tests/test_import_foli_stops.py @@ -5,7 +5,7 @@ from mobility_data.models import ContentType, MobileUnit -from .utils import get_json_data +from .utils import get_test_fixture_json_data @pytest.mark.django_db @@ -13,7 +13,7 @@ def test_import_foli_stops(fetch_json_mock): from mobility_data.importers import foli_stops - fetch_json_mock.return_value = get_json_data("foli_stops.json") + fetch_json_mock.return_value = get_test_fixture_json_data("foli_stops.json") objects = foli_stops.get_foli_stops() foli_stops.save_to_database(objects) assert ContentType.objects.count() == 1 @@ -23,7 +23,6 @@ def test_import_foli_stops(fetch_json_mock): assert turun_satama.content_type == ContentType.objects.first() assert turun_satama.extra["stop_code"] == "1" assert turun_satama.extra["wheelchair_boarding"] == 0 - point = Point(22.21966, 60.43497, srid=4326) point_turun_satama = turun_satama.geometry point_turun_satama.transform(4326) - assert point_turun_satama == point + assert point_turun_satama == Point(22.21966, 60.43497, srid=4326) From 0e0b8fe30b5815228e8e6575891b977d23c5e5a4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 25 Jan 2023 14:07:31 +0200 Subject: [PATCH 018/188] =?UTF-8?q?Add=20tests=20for=20F=C3=B6li=20park=20?= =?UTF-8?q?and=20ride=20stops=20importer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../test_import_foli_parkandride_stops.py | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 mobility_data/tests/test_import_foli_parkandride_stops.py diff --git a/mobility_data/tests/test_import_foli_parkandride_stops.py b/mobility_data/tests/test_import_foli_parkandride_stops.py new file mode 100644 index 000000000..d07fa5907 --- /dev/null +++ b/mobility_data/tests/test_import_foli_parkandride_stops.py @@ -0,0 +1,63 @@ +from unittest.mock import patch + +import pytest + +from mobility_data.models import ContentType, MobileUnit + +from .utils import get_test_fixture_json_data + + +@pytest.mark.django_db +@patch("mobility_data.importers.utils.fetch_json") +def test_import_foli_stops(fetch_json_mock, municipalities): + from mobility_data.importers.foli_parkandride_stop import ( + FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME, + FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME, + get_parkandride_stop_objects, + save_to_database, + ) + + fetch_json_mock.return_value = get_test_fixture_json_data( + "foli_parkandride_stops.json" + ) + car_stops, bike_stops = get_parkandride_stop_objects() + save_to_database(car_stops, FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME) + save_to_database(bike_stops, FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME) + + cars_stops_content_type = ContentType.objects.get( + name=FOLI_PARKANDRIDE_CARS_STOP_CONTENT_TYPE_NAME + ) + bikes_stops_content_type = ContentType.objects.get( + name=FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME + ) + + assert cars_stops_content_type + assert bikes_stops_content_type + # Fixture data contains two park and ride stops for cars and bikes. + assert MobileUnit.objects.filter(content_type=cars_stops_content_type).count() == 2 + assert MobileUnit.objects.filter(content_type=bikes_stops_content_type).count() == 2 + # Test Föli park and ride cars stop + lieto_centre = MobileUnit.objects.get(name_en="Lieto centre, K-Supermarket Lietori") + assert lieto_centre.content_type == cars_stops_content_type + assert lieto_centre.name_fi == "Lieto Keskusta, K-Supermarket Lietorin piha" + assert lieto_centre.name_sv == "Lundo centrum, K-Supermarket Lietori" + assert lieto_centre.address_zip == "21420" + assert ( + lieto_centre.description + == "Lieto Keskusta, K-Supermarket Lietorin piha\nHyvättyläntie 2, 21420" + ) + assert lieto_centre.address_fi == "Hyvättyläntie 2" + assert lieto_centre.address_sv == "Hyvättyläntie 2" + assert lieto_centre.address_en == "Hyvättyläntie 2" + assert lieto_centre.municipality.name == "Lieto" + # Test Föli park and ride bikes stop + raisio_st1 = MobileUnit.objects.get(name_en="St1 Raisio") + assert raisio_st1.content_type == bikes_stops_content_type + assert raisio_st1.name_fi == "St1 Raisio" + assert raisio_st1.name_sv == "St1 Raisio" + assert raisio_st1.address_zip == "21200" + assert raisio_st1.description == "St1 Raisio\nKirkkoväärtinkuja 2, 21200" + assert raisio_st1.municipality.name == "Raisio" + assert raisio_st1.address_fi == "Kirkkoväärtinkuja 2" + assert raisio_st1.address_sv == "Kirkkoväärtinkuja 2" + assert raisio_st1.address_en == "Kirkkoväärtinkuja 2" From 2f374e8f9ecc0db992b2d30f179fe4334e2de915 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 10:03:44 +0200 Subject: [PATCH 019/188] Set content type name as arg to delete_mobile_units --- .../management/commands/import_lounaistieto_shapefiles.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobility_data/management/commands/import_lounaistieto_shapefiles.py b/mobility_data/management/commands/import_lounaistieto_shapefiles.py index 3140af412..d7806e01a 100644 --- a/mobility_data/management/commands/import_lounaistieto_shapefiles.py +++ b/mobility_data/management/commands/import_lounaistieto_shapefiles.py @@ -7,7 +7,6 @@ import_lounaistieto_data_source, ) from mobility_data.importers.utils import delete_mobile_units, get_root_dir -from mobility_data.models import ContentType from ._base_import_command import BaseImportCommand @@ -31,7 +30,8 @@ def handle(self, *args, **options): content_type = options["delete_data_source"] if len(content_type) == 0: logger.warning("Specify the content type to delete.") - delete_mobile_units(getattr(ContentType, content_type[0])) + delete_mobile_units(content_type[0]) + logger.info(f"Deleted MobileUnit and ContenType for {content_type[0]}") else: config_path = f"{get_root_dir()}/mobility_data/importers/data/" path = os.path.join(config_path, CONFIG_FILE) From a38603494434825863c95b1ad71cacc9563dd1b0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 10:05:05 +0200 Subject: [PATCH 020/188] Make fields optional --- mobility_data/importers/lounaistieto_shapefiles.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mobility_data/importers/lounaistieto_shapefiles.py b/mobility_data/importers/lounaistieto_shapefiles.py index ebcd5f6e1..0eff34111 100644 --- a/mobility_data/importers/lounaistieto_shapefiles.py +++ b/mobility_data/importers/lounaistieto_shapefiles.py @@ -78,11 +78,12 @@ def add_feature(self, feature, config, srid): id=municipality_id ).first() - for attr, field in config["fields"].items(): - for lang, field_name in field.items(): - # attr can have fallback definitons if None - if getattr(self, attr)[lang] is None: - getattr(self, attr)[lang] = feature.record[field_name] + if "fields" in config: + for attr, field in config["fields"].items(): + for lang, field_name in field.items(): + # attr can have fallback definitons if None + if getattr(self, attr)[lang] is None: + getattr(self, attr)[lang] = feature.record[field_name] if "extra_fields" in config: for attr, field in config["extra_fields"].items(): From d63dd1d44650af9e601e59f14786f72c9ff68d8c Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 11:41:02 +0200 Subject: [PATCH 021/188] Add SOUTHWEST_FINLAND_GEOMETRY constant --- mobility_data/importers/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobility_data/importers/constants.py b/mobility_data/importers/constants.py index a501bfd59..12f05f266 100644 --- a/mobility_data/importers/constants.py +++ b/mobility_data/importers/constants.py @@ -1,3 +1,5 @@ +from django.contrib.gis.geos import Polygon + SOUTHWEST_FINLAND_BOUNDARY_SRID = 4236 SOUTHWEST_FINLAND_BOUNDARY = [ [20.377543, 60.876637], @@ -12764,3 +12766,7 @@ [20.437571, 60.901807], [20.377543, 60.876637], ] + +SOUTHWEST_FINLAND_GEOMETRY = Polygon( + SOUTHWEST_FINLAND_BOUNDARY, srid=SOUTHWEST_FINLAND_BOUNDARY_SRID +) From 78b9e4409a33beae9c1808448338839f0052cdcd Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 11:44:05 +0200 Subject: [PATCH 022/188] Use SOUTHWEST_FINLAND_GEOMETRY constant --- mobility_data/importers/gas_filling_station.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/mobility_data/importers/gas_filling_station.py b/mobility_data/importers/gas_filling_station.py index 18481916c..322e415b6 100644 --- a/mobility_data/importers/gas_filling_station.py +++ b/mobility_data/importers/gas_filling_station.py @@ -2,11 +2,11 @@ from django import db from django.conf import settings -from django.contrib.gis.geos import Point, Polygon +from django.contrib.gis.geos import Point from mobility_data.models import MobileUnit -from .constants import SOUTHWEST_FINLAND_BOUNDARY, SOUTHWEST_FINLAND_BOUNDARY_SRID +from .constants import SOUTHWEST_FINLAND_GEOMETRY from .utils import ( delete_mobile_units, fetch_json, @@ -75,10 +75,11 @@ def get_filtered_gas_filling_station_objects(json_data=None): srid = 4326 # Create list of all GasFillingStation objects objects = [GasFillingStation(data, srid=srid) for data in json_data["features"]] - # Filter objects by their location - # Polygon used the detect if point intersects. i.e. is in the boundaries of SouthWest Finland. - polygon = Polygon(SOUTHWEST_FINLAND_BOUNDARY, srid=SOUTHWEST_FINLAND_BOUNDARY_SRID) - filtered_objects = [o for o in objects if polygon.intersects(o.point)] + # Filter objects by their location. Polygon(SOUTHWEST_FINLAND_GEOMETRY) used the detect if point intersects. i.e. + # is in the boundaries of SouthWest Finland. + filtered_objects = [ + o for o in objects if SOUTHWEST_FINLAND_GEOMETRY.intersects(o.point) + ] logger.info( "Filtered: {} gas filling stations by location to: {}.".format( len(json_data["features"]), len(filtered_objects) From 64b5278bbd1e3618eca8fd44b997be7e9cc71a0b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 11:54:46 +0200 Subject: [PATCH 023/188] Add data source CommonFerryRoute --- .../data/lounaistieto_shapefiles_config.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml index 38c4f2cf9..1aa3cba0b 100644 --- a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml +++ b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml @@ -1,4 +1,17 @@ data_sources: + - content_type_name: CommonFerryRoute + content_type_description: "Common ferry routes(yhteysalusreitti) in Southwest Finland." + data_url: "https://data.lounaistieto.fi/data/dataset/ea4dcae2-5832-403c-bf7e-b19783ee9a70/resource/faa02c52-2da8-4603-b8bf-4afca42d39a5/download/yhteysalusreitit_laiturit.zip/Yhteysalusreitit.shp" + encoding: latin_1 + filter_by_southwest_finland: True + fields: + name: + fi: REITTIALUE + sv: REITTIALUE + en: REITTIALUE + extra_fields: + yhteysalus: Yhteysalus + - content_type_name: FishingSpot content_type_description: "Fishing spots in Soutwest Finland." data_url: 'https://data.lounaistieto.fi/data/dataset/50451034-b760-4b39-8f88-701d9c968d88/resource/cd41b91f-f684-4cdc-bbf8-e2c4eaca3a70/download/kalapaikat.zip' From 8c5b2bc31e67f08fbfb360c4e2586ebc42fd56f5 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 11:55:24 +0200 Subject: [PATCH 024/188] Add option to filter geometry by Southwest Finland --- mobility_data/importers/lounaistieto_shapefiles.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mobility_data/importers/lounaistieto_shapefiles.py b/mobility_data/importers/lounaistieto_shapefiles.py index 0eff34111..dec07aeb8 100644 --- a/mobility_data/importers/lounaistieto_shapefiles.py +++ b/mobility_data/importers/lounaistieto_shapefiles.py @@ -13,8 +13,10 @@ ) from mobility_data.models import MobileUnit -logger = logging.getLogger("mobility_data") +from .constants import SOUTHWEST_FINLAND_GEOMETRY +logger = logging.getLogger("mobility_data") +SOUTHWEST_FINLAND_GEOMETRY.transform(settings.DEFAULT_SRID) DEFAULT_ENCODING = "utf-8" @@ -70,6 +72,11 @@ def add_feature(self, feature, config, srid): except Exception as e: logger.warning(f"Skipping feature {feature.geom}, invalid geom {e}") return False + + if config.get("filter_by_southwest_finland", False): + if not SOUTHWEST_FINLAND_GEOMETRY.covers(geometry): + return False + if "municipality" in config: municipality = feature.record[config["municipality"]] if municipality: From 41710d034f7862917ce7ef9b3a5f1b11c6b5aa50 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 12:21:09 +0200 Subject: [PATCH 025/188] Add data source FerryDock --- .../data/lounaistieto_shapefiles_config.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml index 1aa3cba0b..c135b41a4 100644 --- a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml +++ b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml @@ -1,4 +1,19 @@ data_sources: + - content_type_name: FerryDock + content_type_description: "Ferry docks in Southwest Finland." + data_url: "https://data.lounaistieto.fi/data/dataset/ea4dcae2-5832-403c-bf7e-b19783ee9a70/resource/faa02c52-2da8-4603-b8bf-4afca42d39a5/download/yhteysalusreitit_laiturit.zip/Laiturit.shp" + encoding: latin_1 + filter_by_southwest_finland: True + fields: + name: + fi: NIMI_ + municipality: KUNTA_1 + extra_fields: + nro: NRO + luokitus: LUOKITUS + alue: alue + reittialue: REITTIALUE + - content_type_name: CommonFerryRoute content_type_description: "Common ferry routes(yhteysalusreitti) in Southwest Finland." data_url: "https://data.lounaistieto.fi/data/dataset/ea4dcae2-5832-403c-bf7e-b19783ee9a70/resource/faa02c52-2da8-4603-b8bf-4afca42d39a5/download/yhteysalusreitit_laiturit.zip/Yhteysalusreitit.shp" From c55a70c11c091f0e6323bf5484bc964a96c90415 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 13:57:12 +0200 Subject: [PATCH 026/188] Add feature BarbecuePlace --- .../importers/data/wfs_importer_config.yml | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/mobility_data/importers/data/wfs_importer_config.yml b/mobility_data/importers/data/wfs_importer_config.yml index 345897fe6..2201b566d 100644 --- a/mobility_data/importers/data/wfs_importer_config.yml +++ b/mobility_data/importers/data/wfs_importer_config.yml @@ -1,4 +1,41 @@ features: + - content_type_name: BarbecuePlace + content_type_description: Barbecue places in the city of Turku. + wfs_layer: GIS:Varusteet + max_features: 100000 + include: + Tyyppi: Grillipaikka + extra_fields: + valmistaja: + wfs_field: Valmistaja + valmistaja_koodi: + wfs_field: Valmistaja_koodi + wfs_type: int + malli: + wfs_field: Malli + malli_koodi: + wfs_field: Malli_koodi + wfs_type: int + hankintavuosi: + wfs_field: Hankintavuosi + wfs_type: int + kunto: + wfs_field: Kunto + kunto_koodi: + wfs_field: Kunto_koodi + wfs_type: int + lukumaara: + wfs_field: Lukumaara + wfs_type: int + pinta-ala: + wfs_field: Pinta-ala + wfs_type: double + pituus: + wfs_field: Pituus + wfs_type: double + asennus: + wfs_field: Asennus + - content_type_name: TicketMachineSign content_type_description: Ticket machine signs in the city of Turku. wfs_layer: GIS:Liikennemerkit From f07e32cc60954a864dfbbb763d7ece7a9ddc2133 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 14:09:16 +0200 Subject: [PATCH 027/188] Add info about importing barbecue places --- mobility_data/README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/README.md b/mobility_data/README.md index 4a64680e3..7b7edd267 100644 --- a/mobility_data/README.md +++ b/mobility_data/README.md @@ -160,6 +160,11 @@ To import data type: ./manage.py import_foli_stops ``` +### Barbecue places +``` +./manage.py import_wfs BarbecuePlace +``` + ### Outdoor gym devices Imports the outdoor gym devices from the services.unit model. i.e., sets references by id to the services.unit model. The data is then serialized from the services.unit model. ``` From 391799b5c7496a9cfac40b3f88363ad697850f96 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 26 Jan 2023 14:10:11 +0200 Subject: [PATCH 028/188] Add task that import barbecue places --- mobility_data/tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/tasks.py b/mobility_data/tasks.py index 3e2ab1994..bd4b634f1 100644 --- a/mobility_data/tasks.py +++ b/mobility_data/tasks.py @@ -41,6 +41,11 @@ def import_accessories(name="import_accessories"): ) +@shared_task +def import_barbecue_places(name="import_barbecue_places"): + management.call_command("import_wfs", ["BarbecuePlace"]) + + @shared_task def import_share_car_parking_places(name="impor_share_car_parking_places"): management.call_command("import_share_car_parking_places") From 9299174e9ba5a188586d1e1c48f5a0bc7de49613 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:21:40 +0200 Subject: [PATCH 029/188] Add DataSource constants for parking machine --- mobility_data/constants.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/constants.py b/mobility_data/constants.py index 3489e2c08..754e2af74 100644 --- a/mobility_data/constants.py +++ b/mobility_data/constants.py @@ -12,6 +12,7 @@ from mobility_data.importers.loading_unloading_places import ( CONTENT_TYPE_NAME as LOADING_UNLOADING_PLACE, ) +from mobility_data.importers.parking_machine import CONTENT_TYPE_NAME as PARKING_MACHINE from mobility_data.importers.share_car_parking_places import ( CONTENT_TYPE_NAME as SHARE_CAR_PARKING_PLACE, ) @@ -48,4 +49,8 @@ "display_name": "berths", "to_services_list": False, }, + PARKING_MACHINE: { + "importer_name": "parking_machines", + "to_services_list": False, + }, } From 2ab6a1072875c97322f48e808459957fa1555542 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:22:38 +0200 Subject: [PATCH 030/188] Add info about parking machines --- mobility_data/README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mobility_data/README.md b/mobility_data/README.md index 338a959ce..87abb32e5 100644 --- a/mobility_data/README.md +++ b/mobility_data/README.md @@ -159,17 +159,24 @@ To import data type: ``` ./manage.py import_foli_stops ``` + ### Föli park and ride stop Imports park and ride stops for bikes and cars. ``` ./manage.py import_foli_parkandride_stops ``` + ### Outdoor gym devices Imports the outdoor gym devices from the services.unit model. i.e., sets references by id to the services.unit model. The data is then serialized from the services.unit model. ``` ./manage.py import_outdoor_gym_devices ``` +### Parking machines +``` +./manage.py import_parking_machines +``` + ## Deletion To delete mobile units for a content type. ``` From 5517039914d9a7a3ca69d5a3a636d14a23681206 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:23:17 +0200 Subject: [PATCH 031/188] Add task to import parking machines --- mobility_data/tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/tasks.py b/mobility_data/tasks.py index f5243c6a3..71d1ba787 100644 --- a/mobility_data/tasks.py +++ b/mobility_data/tasks.py @@ -134,6 +134,11 @@ def import_wfs(args=None, name="import_wfs"): management.call_command("import_wfs", args) +@shared_task +def import_parking_machines(name="import_parking_machines"): + management.call_command("import_parking_machines") + + @shared_task def delete_deprecated_units(name="delete_deprecated_units"): management.call_command("delete_deprecated_units") From 640a41cd5e07b8b9a58f2a86ca867a819bec9eba Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:23:44 +0200 Subject: [PATCH 032/188] Add initial source data --- mobility_data/data/parking_machines.geojson | 103 ++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 mobility_data/data/parking_machines.geojson diff --git a/mobility_data/data/parking_machines.geojson b/mobility_data/data/parking_machines.geojson new file mode 100644 index 000000000..241184713 --- /dev/null +++ b/mobility_data/data/parking_machines.geojson @@ -0,0 +1,103 @@ +{ + "type": "FeatureCollection", + "name": "Automaatit", + "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, + "features": [ + { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, + { "type": "Feature", "properties": { "id": 2, "Osoite": "Puutarhakatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.25417454427166, 60.450266480867569 ] } }, + { "type": "Feature", "properties": { "id": 3, "Osoite": "Puutarhakatu 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.252808192099611, 60.449604877035185 ] } }, + { "type": "Feature", "properties": { "id": 4, "Osoite": "Käsityöläiskatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.257221846094581, 60.450184315580088 ] } }, + { "type": "Feature", "properties": { "id": 5, "Osoite": "Humalistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.259980071100344, 60.450177339116053 ] } }, + { "type": "Feature", "properties": { "id": 6, "Osoite": "Eerikinkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26218847141747, 60.449239333253786 ] } }, + { "type": "Feature", "properties": { "id": 7, "Osoite": "Eerikinkatu 25", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.259787917167984, 60.448419897985524 ] } }, + { "type": "Feature", "properties": { "id": 8, "Osoite": "Käsityöläiskatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.258768788015548, 60.448817764771036 ] } }, + { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, + { "type": "Feature", "properties": { "id": 10, "Osoite": "Rauhankatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.252106097225958, 60.450698403933671 ] } }, + { "type": "Feature", "properties": { "id": 11, "Osoite": "Rauhankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255241933856727, 60.451958158547647 ] } }, + { "type": "Feature", "properties": { "id": 12, "Osoite": "Rauhankatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.257154615291935, 60.452441782474324 ] } }, + { "type": "Feature", "properties": { "id": 13, "Osoite": "Humalistonkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.257216154392239, 60.451862555940885 ] } }, + { "type": "Feature", "properties": { "id": 14, "Osoite": "Läntinen pitkäkatu 24", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255051468343908, 60.452987302517734 ] } }, + { "type": "Feature", "properties": { "id": 15, "Osoite": "Läntinen pitkäkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256323063276643, 60.453437911269269 ] } }, + { "type": "Feature", "properties": { "id": 16, "Osoite": "Läntinen pitkäkatu 31", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.257994466697046, 60.454187956221354 ] } }, + { "type": "Feature", "properties": { "id": 17, "Osoite": "Läntinen pitkäkatu 23", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.261165193612193, 60.455262424830082 ] } }, + { "type": "Feature", "properties": { "id": 18, "Osoite": "Läntinen pitkäkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.264365559509113, 60.456145444039713 ] } }, + { "type": "Feature", "properties": { "id": 19, "Osoite": "Läntinen pitkäkatu 9", "Sijainti": "Viheralue", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265258802895602, 60.456688407044439 ] } }, + { "type": "Feature", "properties": { "id": 20, "Osoite": "Linja-autoasema", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.26823862990101, 60.456618323168733 ] } }, + { "type": "Feature", "properties": { "id": 21, "Osoite": "Brahenkatu 18-20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265432860869307, 60.45609461101597 ] } }, + { "type": "Feature", "properties": { "id": 22, "Osoite": "Tuureporinkatu 13", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.267508058545992, 60.455899614161197 ] } }, + { "type": "Feature", "properties": { "id": 23, "Osoite": "Tuureporinkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265579060229619, 60.45501128227162 ] } }, + { "type": "Feature", "properties": { "id": 24, "Osoite": "Kauppiaskatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.263927120185748, 60.455138542084491 ] } }, + { "type": "Feature", "properties": { "id": 25, "Osoite": "Kauppiaskatu 17", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.265027327999253, 60.454324227152817 ] } }, + { "type": "Feature", "properties": { "id": 26, "Osoite": "Puutori", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "26.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268197772640576, 60.455031569350027 ] } }, + { "type": "Feature", "properties": { "id": 27, "Osoite": "Maariankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.269849896888495, 60.454839172502602 ] } }, + { "type": "Feature", "properties": { "id": 28, "Osoite": "Brahenkatu 10", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268764353572763, 60.453721815846308 ] } }, + { "type": "Feature", "properties": { "id": 29, "Osoite": "Brahenkatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.270232241076812, 60.452956273815545 ] } }, + { "type": "Feature", "properties": { "id": 30, "Osoite": "Yliopistonkatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.270612920328336, 60.453720163907732 ] } }, + { "type": "Feature", "properties": { "id": 31, "Osoite": "Nahkurinkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.27195361111416, 60.454879189541835 ] } }, + { "type": "Feature", "properties": { "id": 32, "Osoite": "Multavierunkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274649677455383, 60.454469280317213 ] } }, + { "type": "Feature", "properties": { "id": 33, "Osoite": "Eerikinkatu 3 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274411237269273, 60.453231364393154 ] } }, + { "type": "Feature", "properties": { "id": 34, "Osoite": "Aurakatu 22", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26318967618387, 60.452955972059186 ] } }, + { "type": "Feature", "properties": { "id": 35, "Osoite": "Maariankatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "17.12.2021", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.266164317389642, 60.453604384909475 ] } }, + { "type": "Feature", "properties": { "id": 36, "Osoite": "Puolalankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.261904591298897, 60.452405246993045 ] } }, + { "type": "Feature", "properties": { "id": 37, "Osoite": "Puolalankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.262568837946596, 60.451667570101037 ] } }, + { "type": "Feature", "properties": { "id": 38, "Osoite": "Kauppahalli", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.265780082893588, 60.449524771568896 ] } }, + { "type": "Feature", "properties": { "id": 39, "Osoite": "Linnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.273690297937019, 60.451923978676078 ] } }, + { "type": "Feature", "properties": { "id": 40, "Osoite": "Aurakatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268066143927651, 60.44930830363591 ] } }, + { "type": "Feature", "properties": { "id": 41, "Osoite": "Kauppiaskatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.27064815615228, 60.450128994763617 ] } }, + { "type": "Feature", "properties": { "id": 42, "Osoite": "Linnankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.271717105625264, 60.451106984503213 ] } }, + { "type": "Feature", "properties": { "id": 43, "Osoite": "Borenpuisto/aukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256426088884556, 60.445362011920302 ] } }, + { "type": "Feature", "properties": { "id": 44, "Osoite": "Eerikinkatu 38", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.254031506456919, 60.446270987052962 ] } }, + { "type": "Feature", "properties": { "id": 45, "Osoite": "Eerikinkatu 40", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.252807701852902, 60.44585626467363 ] } }, + { "type": "Feature", "properties": { "id": 46, "Osoite": "Sairashuoneenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.251775173461883, 60.444872332201818 ] } }, + { "type": "Feature", "properties": { "id": 47, "Osoite": "Puistokatu 12", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.250455226870177, 60.447872484202826 ] } }, + { "type": "Feature", "properties": { "id": 48, "Osoite": "Itsenäisyydenaukio 2 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.262710182022698, 60.445569236961369 ] } }, + { "type": "Feature", "properties": { "id": 50, "Osoite": "Itäinen pitkäkatu/Olavinpuisto vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.269007832110184, 60.447885003377571 ] } }, + { "type": "Feature", "properties": { "id": 51, "Osoite": "Kaskenkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.271248289746925, 60.447359092619095 ] } }, + { "type": "Feature", "properties": { "id": 52, "Osoite": "Hämeenkatu 28", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.272740939586971, 60.448712477636697 ] } }, + { "type": "Feature", "properties": { "id": 53, "Osoite": "Hämeenkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274129556670403, 60.449348582880177 ] } }, + { "type": "Feature", "properties": { "id": 54, "Osoite": "Hämeenkatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.27594945421551, 60.449947345346075 ] } }, + { "type": "Feature", "properties": { "id": 55, "Osoite": "Nunnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.273923382374562, 60.450153277326237 ] } }, + { "type": "Feature", "properties": { "id": 56, "Osoite": "Uudenmaankatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279477989161659, 60.449038532856854 ] } }, + { "type": "Feature", "properties": { "id": 57, "Osoite": "Vähä Hämeenkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279883914491133, 60.449633311242088 ] } }, + { "type": "Feature", "properties": { "id": 58, "Osoite": "Hovioikeudenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280643577273363, 60.450943454869588 ] } }, + { "type": "Feature", "properties": { "id": 59, "Osoite": "Tuomiokirkontori 1-3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.277983862006817, 60.451173921939066 ] } }, + { "type": "Feature", "properties": { "id": 60, "Osoite": "Tuomiokirkonkatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.280249199106677, 60.451952316001226 ] } }, + { "type": "Feature", "properties": { "id": 61, "Osoite": "Kerttulinkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.282016000564631, 60.452352972963375 ] } }, + { "type": "Feature", "properties": { "id": 62, "Osoite": "Hämeenkatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.283591614208678, 60.452205864044352 ] } }, + { "type": "Feature", "properties": { "id": 63, "Osoite": "Rehtorinpellonkatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.287462642661801, 60.453937416413524 ] } }, + { "type": "Feature", "properties": { "id": 64, "Osoite": "Vähä Hämeenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.28764674597139, 60.452178003199926 ] } }, + { "type": "Feature", "properties": { "id": 65, "Osoite": "Lemminkäisenkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.294781520449281, 60.448205452316486 ] } }, + { "type": "Feature", "properties": { "id": 66, "Osoite": "Lemminkäisenkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.296182597025911, 60.447646306211979 ] } }, + { "type": "Feature", "properties": { "id": 67, "Osoite": "Puutarhakatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.260430472827235, 60.45216772994651 ] } }, + { "type": "Feature", "properties": { "id": 68, "Osoite": "Humalistonkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "29.1.2018", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.258675701953273, 60.450865575940362 ] } }, + { "type": "Feature", "properties": { "id": 69, "Osoite": "Puutarhakatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26175837633734, 60.45287615088543 ] } }, + { "type": "Feature", "properties": { "id": 70, "Osoite": "Käsityöläiskatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "27.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.25604659396264, 60.450985440937544 ] } }, + { "type": "Feature", "properties": { "id": 71, "Osoite": "Yliopistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT Touch", "Asennettu": "11.10.2022", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.273213346106335, 60.454614196330851 ] } }, + { "type": "Feature", "properties": { "id": 72, "Osoite": "Itäinen Rantakatu 72", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.243079545713517, 60.437818487078545 ] } }, + { "type": "Feature", "properties": { "id": 73, "Osoite": "Kasarminkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280352370046622, 60.457669207319114 ] } }, + { "type": "Feature", "properties": { "id": 75, "Osoite": "Linnankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.264872376936172, 60.448691040677524 ] } }, + { "type": "Feature", "properties": { "id": 77, "Osoite": "Piispankatu 10", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279733274298824, 60.455020619216043 ] } }, + { "type": "Feature", "properties": { "id": 78, "Osoite": "Piispankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale§", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.277760964164081, 60.453216079342297 ] } }, + { "type": "Feature", "properties": { "id": 79, "Osoite": "Porthaninkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.28073811293207, 60.453918201523102 ] } }, + { "type": "Feature", "properties": { "id": 84, "Osoite": "Tehtaankatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280930555107421, 60.45626865766279 ] } }, + { "type": "Feature", "properties": { "id": 87, "Osoite": "Ursiininkatu 9", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256546650434711, 60.448655233433726 ] } }, + { "type": "Feature", "properties": { "id": 88, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.29275674703927, 60.443803487057082 ] } }, + { "type": "Feature", "properties": { "id": 89, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.294113833178297, 60.442723807754163 ] } }, + { "type": "Feature", "properties": { "id": 90, "Osoite": "Tahkonaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.294175006719268, 60.446804509763439 ] } }, + { "type": "Feature", "properties": { "id": 91, "Osoite": "Teollisuuskatu 16", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT Touch", "Asennettu": "11.10.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.301980689265612, 60.449590340262404 ] } }, + { "type": "Feature", "properties": { "id": 300, "Osoite": "Kunnallissairaalantie 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.275377662478608, 60.440746141486549 ] } }, + { "type": "Feature", "properties": { "id": 301, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.274444434528675, 60.438998327974652 ] } }, + { "type": "Feature", "properties": { "id": 302, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.274072124365258, 60.439409437701471 ] } }, + { "type": "Feature", "properties": { "id": 401, "Osoite": "4. linja ", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "13 €/26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.2227213543412, 60.435316415968622 ] } }, + { "type": "Feature", "properties": { "id": 403, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.221447167201752, 60.435584779574739 ] } }, + { "type": "Feature", "properties": { "id": 404, "Osoite": "3. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.221389745850257, 60.435173290652273 ] } }, + { "type": "Feature", "properties": { "id": 405, "Osoite": "Linnankatu 91", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.219860237204227, 60.435680281344297 ] } }, + { "type": "Feature", "properties": { "id": 406, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.224974204395551, 60.433923001505228 ] } }, + { "type": "Feature", "properties": { "id": 407, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225946466932712, 60.434033761861251 ] } }, + { "type": "Feature", "properties": { "id": 408, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.224755030627708, 60.434824489085884 ] } }, + { "type": "Feature", "properties": { "id": 409, "Osoite": "Linnankatu 87", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225127681727219, 60.435830090490398 ] } }, + { "type": "Feature", "properties": { "id": 413, "Osoite": "Satamakatu 18", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225972071802026, 60.436572492254285 ] } }, + { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } + ] + } + \ No newline at end of file From 56681c51ab7342b282d3114ba6a4b509fb9751f9 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:24:04 +0200 Subject: [PATCH 033/188] Refactor --- mobility_data/importers/foli_stops.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mobility_data/importers/foli_stops.py b/mobility_data/importers/foli_stops.py index 652d850f1..aa5d8b769 100644 --- a/mobility_data/importers/foli_stops.py +++ b/mobility_data/importers/foli_stops.py @@ -29,10 +29,7 @@ def __init__(self, stop_data): def get_foli_stops(): json_data = fetch_json(URL) - objects = [] - for stop_code in json_data: - objects.append(FoliStop(json_data[stop_code])) - return objects + return [FoliStop(json_data[stop_code]) for stop_code in json_data] @db.transaction.atomic From c7d1e58bdaffcbd0a9f17763cd62eba6b159da41 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:24:21 +0200 Subject: [PATCH 034/188] Add importer for parking machines --- mobility_data/importers/parking_machine.py | 86 ++++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 mobility_data/importers/parking_machine.py diff --git a/mobility_data/importers/parking_machine.py b/mobility_data/importers/parking_machine.py new file mode 100644 index 000000000..777b61f54 --- /dev/null +++ b/mobility_data/importers/parking_machine.py @@ -0,0 +1,86 @@ +from django import db +from django.conf import settings +from django.contrib.gis.gdal import DataSource as GDALDataSource +from django.contrib.gis.geos import GEOSGeometry + +from mobility_data.models import MobileUnit + +from .utils import ( + delete_mobile_units, + FieldTypes, + get_file_name_from_data_source, + get_or_create_content_type, + get_root_dir, +) + +SOURCE_DATA_SRID = 4326 +GEOJSON_FILENAME = "parking_machines.geojson" +CONTENT_TYPE_NAME = "ParkingMachine" + + +class ParkingMachine: + extra_field_mappings = { + "Sijainti": {"type": FieldTypes.STRING}, + "Valmistaja": {"type": FieldTypes.STRING}, + "Malli": {"type": FieldTypes.STRING}, + "Asennettu": {"type": FieldTypes.STRING}, + "Virta": {"type": FieldTypes.STRING}, + "Maksutapa": {"type": FieldTypes.STRING}, + "Näyttö": {"type": FieldTypes.STRING}, + "Muuta": {"type": FieldTypes.STRING}, + "Omistaja": {"type": FieldTypes.STRING}, + "Taksa/h": {"type": FieldTypes.FLOAT}, + "Max.aika": {"type": FieldTypes.FLOAT}, + "Maksuvyöhyke": {"type": FieldTypes.STRING}, + } + + def __init__(self, feature): + self.extra = {} + self.address = feature["Osoite"].as_string() + self.geometry = GEOSGeometry(feature.geom.wkt, srid=SOURCE_DATA_SRID) + self.geometry.transform(settings.DEFAULT_SRID) + for field in feature.fields: + if field in self.extra_field_mappings: + match self.extra_field_mappings[field]["type"]: + case FieldTypes.STRING: + self.extra[field] = feature[field].as_string() + case FieldTypes.INTEGER: + self.extra[field] = feature[field].as_int() + case FieldTypes.FLOAT: + self.extra[field] = feature[field].as_double() + + +def get_data_layer(): + file_name = get_file_name_from_data_source(CONTENT_TYPE_NAME) + if not file_name: + file_name = f"{get_root_dir()}/mobility_data/data/{GEOJSON_FILENAME}" + ds = GDALDataSource(file_name) + assert len(ds) == 1 + return ds[0] + + +def get_parking_machine_objects(): + data_layer = get_data_layer() + return [ParkingMachine(feature) for feature in data_layer] + + +@db.transaction.atomic +def get_and_create_parking_machine_content_type(): + description = "Parking machines in the city of Turku." + content_type, _ = get_or_create_content_type(CONTENT_TYPE_NAME, description) + return content_type + + +@db.transaction.atomic +def save_to_database(objects, delete_tables=True): + if delete_tables: + delete_mobile_units(CONTENT_TYPE_NAME) + + content_type = get_and_create_parking_machine_content_type() + for object in objects: + MobileUnit.objects.create( + content_type=content_type, + address=object.address, + geometry=object.geometry, + extra=object.extra, + ) From 6c286923fa999301d228b0d15af7570b4e3a7ed4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 12:24:52 +0200 Subject: [PATCH 035/188] Add management command to import parking machines --- .../commands/import_parking_machines.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 mobility_data/management/commands/import_parking_machines.py diff --git a/mobility_data/management/commands/import_parking_machines.py b/mobility_data/management/commands/import_parking_machines.py new file mode 100644 index 000000000..f78395c87 --- /dev/null +++ b/mobility_data/management/commands/import_parking_machines.py @@ -0,0 +1,17 @@ +import logging + +from django.core.management import BaseCommand + +from mobility_data.importers.parking_machine import ( + get_parking_machine_objects, + save_to_database, +) + +logger = logging.getLogger("mobility_data") + + +class Command(BaseCommand): + def handle(self, *args, **options): + objects = get_parking_machine_objects() + save_to_database(objects) + logger.info(f"Saved {len(objects)} parking machines to database.") From 96645d38d4ed8f115574a8cc4d91ef110ec330b0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:27:53 +0200 Subject: [PATCH 036/188] Delete, file renamed to parking_machines.py --- mobility_data/importers/parking_machine.py | 86 ---------------------- 1 file changed, 86 deletions(-) delete mode 100644 mobility_data/importers/parking_machine.py diff --git a/mobility_data/importers/parking_machine.py b/mobility_data/importers/parking_machine.py deleted file mode 100644 index 777b61f54..000000000 --- a/mobility_data/importers/parking_machine.py +++ /dev/null @@ -1,86 +0,0 @@ -from django import db -from django.conf import settings -from django.contrib.gis.gdal import DataSource as GDALDataSource -from django.contrib.gis.geos import GEOSGeometry - -from mobility_data.models import MobileUnit - -from .utils import ( - delete_mobile_units, - FieldTypes, - get_file_name_from_data_source, - get_or_create_content_type, - get_root_dir, -) - -SOURCE_DATA_SRID = 4326 -GEOJSON_FILENAME = "parking_machines.geojson" -CONTENT_TYPE_NAME = "ParkingMachine" - - -class ParkingMachine: - extra_field_mappings = { - "Sijainti": {"type": FieldTypes.STRING}, - "Valmistaja": {"type": FieldTypes.STRING}, - "Malli": {"type": FieldTypes.STRING}, - "Asennettu": {"type": FieldTypes.STRING}, - "Virta": {"type": FieldTypes.STRING}, - "Maksutapa": {"type": FieldTypes.STRING}, - "Näyttö": {"type": FieldTypes.STRING}, - "Muuta": {"type": FieldTypes.STRING}, - "Omistaja": {"type": FieldTypes.STRING}, - "Taksa/h": {"type": FieldTypes.FLOAT}, - "Max.aika": {"type": FieldTypes.FLOAT}, - "Maksuvyöhyke": {"type": FieldTypes.STRING}, - } - - def __init__(self, feature): - self.extra = {} - self.address = feature["Osoite"].as_string() - self.geometry = GEOSGeometry(feature.geom.wkt, srid=SOURCE_DATA_SRID) - self.geometry.transform(settings.DEFAULT_SRID) - for field in feature.fields: - if field in self.extra_field_mappings: - match self.extra_field_mappings[field]["type"]: - case FieldTypes.STRING: - self.extra[field] = feature[field].as_string() - case FieldTypes.INTEGER: - self.extra[field] = feature[field].as_int() - case FieldTypes.FLOAT: - self.extra[field] = feature[field].as_double() - - -def get_data_layer(): - file_name = get_file_name_from_data_source(CONTENT_TYPE_NAME) - if not file_name: - file_name = f"{get_root_dir()}/mobility_data/data/{GEOJSON_FILENAME}" - ds = GDALDataSource(file_name) - assert len(ds) == 1 - return ds[0] - - -def get_parking_machine_objects(): - data_layer = get_data_layer() - return [ParkingMachine(feature) for feature in data_layer] - - -@db.transaction.atomic -def get_and_create_parking_machine_content_type(): - description = "Parking machines in the city of Turku." - content_type, _ = get_or_create_content_type(CONTENT_TYPE_NAME, description) - return content_type - - -@db.transaction.atomic -def save_to_database(objects, delete_tables=True): - if delete_tables: - delete_mobile_units(CONTENT_TYPE_NAME) - - content_type = get_and_create_parking_machine_content_type() - for object in objects: - MobileUnit.objects.create( - content_type=content_type, - address=object.address, - geometry=object.geometry, - extra=object.extra, - ) From 4a752654986c59d2d76b37c04d2d2f8ccf08217f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:28:25 +0200 Subject: [PATCH 037/188] Rename to parking_machines.py --- mobility_data/importers/parking_machines.py | 86 +++++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 mobility_data/importers/parking_machines.py diff --git a/mobility_data/importers/parking_machines.py b/mobility_data/importers/parking_machines.py new file mode 100644 index 000000000..777b61f54 --- /dev/null +++ b/mobility_data/importers/parking_machines.py @@ -0,0 +1,86 @@ +from django import db +from django.conf import settings +from django.contrib.gis.gdal import DataSource as GDALDataSource +from django.contrib.gis.geos import GEOSGeometry + +from mobility_data.models import MobileUnit + +from .utils import ( + delete_mobile_units, + FieldTypes, + get_file_name_from_data_source, + get_or_create_content_type, + get_root_dir, +) + +SOURCE_DATA_SRID = 4326 +GEOJSON_FILENAME = "parking_machines.geojson" +CONTENT_TYPE_NAME = "ParkingMachine" + + +class ParkingMachine: + extra_field_mappings = { + "Sijainti": {"type": FieldTypes.STRING}, + "Valmistaja": {"type": FieldTypes.STRING}, + "Malli": {"type": FieldTypes.STRING}, + "Asennettu": {"type": FieldTypes.STRING}, + "Virta": {"type": FieldTypes.STRING}, + "Maksutapa": {"type": FieldTypes.STRING}, + "Näyttö": {"type": FieldTypes.STRING}, + "Muuta": {"type": FieldTypes.STRING}, + "Omistaja": {"type": FieldTypes.STRING}, + "Taksa/h": {"type": FieldTypes.FLOAT}, + "Max.aika": {"type": FieldTypes.FLOAT}, + "Maksuvyöhyke": {"type": FieldTypes.STRING}, + } + + def __init__(self, feature): + self.extra = {} + self.address = feature["Osoite"].as_string() + self.geometry = GEOSGeometry(feature.geom.wkt, srid=SOURCE_DATA_SRID) + self.geometry.transform(settings.DEFAULT_SRID) + for field in feature.fields: + if field in self.extra_field_mappings: + match self.extra_field_mappings[field]["type"]: + case FieldTypes.STRING: + self.extra[field] = feature[field].as_string() + case FieldTypes.INTEGER: + self.extra[field] = feature[field].as_int() + case FieldTypes.FLOAT: + self.extra[field] = feature[field].as_double() + + +def get_data_layer(): + file_name = get_file_name_from_data_source(CONTENT_TYPE_NAME) + if not file_name: + file_name = f"{get_root_dir()}/mobility_data/data/{GEOJSON_FILENAME}" + ds = GDALDataSource(file_name) + assert len(ds) == 1 + return ds[0] + + +def get_parking_machine_objects(): + data_layer = get_data_layer() + return [ParkingMachine(feature) for feature in data_layer] + + +@db.transaction.atomic +def get_and_create_parking_machine_content_type(): + description = "Parking machines in the city of Turku." + content_type, _ = get_or_create_content_type(CONTENT_TYPE_NAME, description) + return content_type + + +@db.transaction.atomic +def save_to_database(objects, delete_tables=True): + if delete_tables: + delete_mobile_units(CONTENT_TYPE_NAME) + + content_type = get_and_create_parking_machine_content_type() + for object in objects: + MobileUnit.objects.create( + content_type=content_type, + address=object.address, + geometry=object.geometry, + extra=object.extra, + ) From 36be636b196a63d285055169dc8596bb2707e976 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:28:57 +0200 Subject: [PATCH 038/188] Add fixture data for tests --- mobility_data/tests/data/parking_machines.geojson | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 mobility_data/tests/data/parking_machines.geojson diff --git a/mobility_data/tests/data/parking_machines.geojson b/mobility_data/tests/data/parking_machines.geojson new file mode 100644 index 000000000..bb26ac30f --- /dev/null +++ b/mobility_data/tests/data/parking_machines.geojson @@ -0,0 +1,11 @@ +{ + "type": "FeatureCollection", + "name": "Automaatit", + "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, + "features": [ + { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, + { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, + { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } + ] + } + \ No newline at end of file From dc9b2a84c1d4652ce0f6b7d8cc1af88ddd6aab9b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:36:03 +0200 Subject: [PATCH 039/188] Change parking_machine to parking_machines --- mobility_data/constants.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobility_data/constants.py b/mobility_data/constants.py index 754e2af74..8d5a03033 100644 --- a/mobility_data/constants.py +++ b/mobility_data/constants.py @@ -12,7 +12,9 @@ from mobility_data.importers.loading_unloading_places import ( CONTENT_TYPE_NAME as LOADING_UNLOADING_PLACE, ) -from mobility_data.importers.parking_machine import CONTENT_TYPE_NAME as PARKING_MACHINE +from mobility_data.importers.parking_machines import ( + CONTENT_TYPE_NAME as PARKING_MACHINE, +) from mobility_data.importers.share_car_parking_places import ( CONTENT_TYPE_NAME as SHARE_CAR_PARKING_PLACE, ) From 11a7975757d8f8111e92921ef59d50acbe3780bb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:37:00 +0200 Subject: [PATCH 040/188] Add function to get fixture data layer --- mobility_data/tests/utils.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/mobility_data/tests/utils.py b/mobility_data/tests/utils.py index 89e3b8a24..8d59f5192 100644 --- a/mobility_data/tests/utils.py +++ b/mobility_data/tests/utils.py @@ -2,6 +2,7 @@ import os from io import StringIO +from django.contrib.gis.gdal import DataSource from django.core.management import call_command @@ -26,3 +27,11 @@ def get_test_fixture_json_data(file_name): with open(file) as f: data = json.load(f) return data + + +def get_test_fixture_data_layer(file_name): + data_path = os.path.join(os.path.dirname(__file__), "data") + file = os.path.join(data_path, file_name) + ds = DataSource(file) + assert len(ds) == 1 + return ds[0] From 93e23e1b6eb3e0a92dedff9454364b4ee56f88fb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:37:35 +0200 Subject: [PATCH 041/188] Change parking_machine to parking_machines --- mobility_data/management/commands/import_parking_machines.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/management/commands/import_parking_machines.py b/mobility_data/management/commands/import_parking_machines.py index f78395c87..fdc52bda3 100644 --- a/mobility_data/management/commands/import_parking_machines.py +++ b/mobility_data/management/commands/import_parking_machines.py @@ -2,7 +2,7 @@ from django.core.management import BaseCommand -from mobility_data.importers.parking_machine import ( +from mobility_data.importers.parking_machines import ( get_parking_machine_objects, save_to_database, ) From 5ec2d2318ccaef6384ca1b798715f4a87ef2fafc Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 27 Jan 2023 13:38:24 +0200 Subject: [PATCH 042/188] Add import_parking_machines tests --- .../tests/test_import_parking_machines.py | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 mobility_data/tests/test_import_parking_machines.py diff --git a/mobility_data/tests/test_import_parking_machines.py b/mobility_data/tests/test_import_parking_machines.py new file mode 100644 index 000000000..46148839a --- /dev/null +++ b/mobility_data/tests/test_import_parking_machines.py @@ -0,0 +1,36 @@ +from unittest.mock import patch + +import pytest + +from mobility_data.models import ContentType, MobileUnit + +from .utils import get_test_fixture_data_layer + + +@pytest.mark.django_db +@patch("mobility_data.importers.parking_machines.get_data_layer") +def test_import_parking_machines(get_data_layer_mock): + from mobility_data.importers import parking_machines + + get_data_layer_mock.return_value = get_test_fixture_data_layer( + "parking_machines.geojson" + ) + objects = parking_machines.get_parking_machine_objects() + parking_machines.save_to_database(objects) + assert ContentType.objects.first().name == parking_machines.CONTENT_TYPE_NAME + assert MobileUnit.objects.count() == 3 + satamakatu = MobileUnit.objects.first() + assert satamakatu.content_type == ContentType.objects.first() + assert satamakatu.address == "Satamakatu 18 vp" + assert satamakatu.extra["Malli"] == "CWT-C Touch" + assert satamakatu.extra["Muuta"] == "16 € / 26 h" + assert satamakatu.extra["Virta"] == "Verkkovirta" + assert satamakatu.extra["Taksa/h"] == 1.3 + assert satamakatu.extra["Max.aika"] is None + assert satamakatu.extra["Näyttö"] == '9", kosketus' + assert satamakatu.extra["Omistaja"] == "Turun kaupunki" + assert satamakatu.extra["Sijainti"] == "Katuosa" + assert satamakatu.extra["Asennettu"] == "15.10.2022" + assert satamakatu.extra["Maksutapa"] == "Kolikko, kortti, lähimaksu" + assert satamakatu.extra["Valmistaja"] == "Cale" + assert satamakatu.extra["Maksuvyöhyke"] is None From 9418bfd831d4a47f7df3552cfb1162bdf6b57f50 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 30 Jan 2023 12:32:01 +0200 Subject: [PATCH 043/188] Fix typo --- .../management/commands/import_lounaistieto_shapefiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/management/commands/import_lounaistieto_shapefiles.py b/mobility_data/management/commands/import_lounaistieto_shapefiles.py index d7806e01a..060b2b3f6 100644 --- a/mobility_data/management/commands/import_lounaistieto_shapefiles.py +++ b/mobility_data/management/commands/import_lounaistieto_shapefiles.py @@ -31,7 +31,7 @@ def handle(self, *args, **options): if len(content_type) == 0: logger.warning("Specify the content type to delete.") delete_mobile_units(content_type[0]) - logger.info(f"Deleted MobileUnit and ContenType for {content_type[0]}") + logger.info(f"Deleted MobileUnit and ContentType for {content_type[0]}") else: config_path = f"{get_root_dir()}/mobility_data/importers/data/" path = os.path.join(config_path, CONFIG_FILE) From ea9591bffd5abfe33c53d0d3f463cdaecb06a881 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 30 Jan 2023 15:59:11 +0200 Subject: [PATCH 044/188] Add base class for importing external sources and functions for loading their configs --- smbackend_turku/importers/utils.py | 108 +++++++++++++++++++++++++++-- 1 file changed, 104 insertions(+), 4 deletions(-) diff --git a/smbackend_turku/importers/utils.py b/smbackend_turku/importers/utils.py index 8b9e31ba0..7ca383ea9 100644 --- a/smbackend_turku/importers/utils.py +++ b/smbackend_turku/importers/utils.py @@ -6,6 +6,7 @@ import pytz import requests +import yaml from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from munigeo.models import ( @@ -15,13 +16,43 @@ Municipality, ) -from services.models import Service, ServiceNode, Unit +from mobility_data.importers.utils import ( + create_mobile_unit_as_unit_reference, + delete_mobile_units, +) +from services.management.commands.services_import.services import ( + update_service_counts, + update_service_node_counts, +) +from services.models import Service, ServiceNode, Unit, UnitServiceDetails # TODO: Change to production endpoint when available TURKU_BASE_URL = "https://digiaurajoki.turku.fi:9443/kuntapalvelut/api/v1/" ACCESSIBILITY_BASE_URL = "https://asiointi.hel.fi/kapaesteettomyys/api/v1/" UTC_TIMEZONE = pytz.timezone("UTC") +data_path = os.path.join(os.path.dirname(__file__), "data") +EXTERNAL_SOURCES_CONFIG_FILE = f"{data_path}/external_units_config.yml" + + +def get_external_sources_yaml_config(): + config = yaml.safe_load(open(EXTERNAL_SOURCES_CONFIG_FILE, "r", encoding="utf-8")) + return config["external_data_sources"] + + +def get_external_source_config(external_source_name): + config = get_external_sources_yaml_config() + for c in config: + if c["name"] == external_source_name: + return c + return None + + +def get_configured_external_sources_names(config=None): + if not config: + config = get_external_sources_yaml_config() + return [f["name"] for f in config] + def clean_text(text, default=None): if not isinstance(text, str): @@ -315,7 +346,9 @@ def create_service(service_id, service_node_id, service_name, service_names): def delete_external_source( - service_id, service_node_id, mobility_data_delete_func=False + service_id, + service_node_id, + mobile_units_content_type_name, ): """ Deletes the data source from services list and optionally from mobility_data. @@ -323,5 +356,72 @@ def delete_external_source( Unit.objects.filter(services__id=service_id).delete() Service.objects.filter(id=service_id).delete() ServiceNode.objects.filter(id=service_node_id).delete() - if mobility_data_delete_func: - mobility_data_delete_func() + delete_mobile_units(mobile_units_content_type_name) + + +class BaseExternalSource: + def __init__(self, config): + self.config = config + self.SERVICE_ID = config["service"]["id"] + self.SERVICE_NODE_ID = config["service_node"]["id"] + self.UNITS_ID_OFFSET = config["units_offset"] + self.SERVICE_NAME = config["service"]["name"]["fi"] + self.SERVICE_NAMES = config["service"]["name"] + self.SERVICE_NODE_NAME = config["service_node"]["name"]["fi"] + self.SERVICE_NODE_NAMES = config["service_node"]["name"] + self.delete_external_source() + create_service_node( + self.config["service_node"]["id"], + self.config["service_node"]["name"]["fi"], + self.config["root_service_node_name"], + self.config["service_node"]["name"], + ) + create_service( + self.config["service"]["id"], + self.config["service_node"]["id"], + self.config["service"]["name"]["fi"], + self.config["service"]["name"], + ) + + def delete_external_source(self): + delete_external_source( + self.config["service"]["id"], + self.config["service_node"]["id"], + self.config["mobility_data_content_type_name"], + ) + + def save_objects_as_units(self, objects, content_type): + for i, object in enumerate(objects): + unit_id = i + self.UNITS_ID_OFFSET + unit = Unit(id=unit_id) + set_field(unit, "location", object.geometry) + set_tku_translated_field(unit, "name", object.name) + set_tku_translated_field(unit, "street_address", object.address) + set_tku_translated_field(unit, "description", object.description) + set_field(unit, "extra", object.extra) + if "provider_type" in self.config: + set_syncher_object_field( + unit, "provider_type", self.config["provider_type"] + ) + try: + service = Service.objects.get(id=self.SERVICE_ID) + except Service.DoesNotExist: + self.logger.warning( + 'Service "{}" does not exist!'.format(self.SERVICE_ID) + ) + continue + UnitServiceDetails.objects.get_or_create(unit=unit, service=service) + service_nodes = ServiceNode.objects.filter(related_services=service) + unit.service_nodes.add(*service_nodes) + set_field(unit, "root_service_nodes", unit.get_root_service_nodes()[0]) + municipality = get_municipality(object.municipality) + set_field(unit, "municipality", municipality) + set_field(unit, "address_zip", object.zip_code) + unit.last_modified_time = datetime.datetime.now(UTC_TIMEZONE) + set_service_names_field(unit) + unit.save() + if self.config.get("create_mobile_unit_as_unit_reference", False): + create_mobile_unit_as_unit_reference(unit_id, content_type) + update_service_node_counts() + update_service_counts() + self.logger.info(f"Imported {len(objects)} {self.config['name']}...") From b099bad11bdad8010798148b2f0e213e4d3a30c7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 30 Jan 2023 16:09:41 +0200 Subject: [PATCH 045/188] Refactor to use super class BaseExternalSource for entity creation --- .../importers/bike_service_stations.py | 99 ++----------------- 1 file changed, 8 insertions(+), 91 deletions(-) diff --git a/smbackend_turku/importers/bike_service_stations.py b/smbackend_turku/importers/bike_service_stations.py index 47389894f..d4ea58ec2 100644 --- a/smbackend_turku/importers/bike_service_stations.py +++ b/smbackend_turku/importers/bike_service_stations.py @@ -1,120 +1,37 @@ -from datetime import datetime - -from django.conf import settings - from mobility_data.importers.bike_service_stations import ( create_bike_service_station_content_type, - delete_bike_service_stations as mobility_data_delete_bike_service_stations, get_bike_service_station_objects, ) -from mobility_data.importers.utils import create_mobile_unit_as_unit_reference from services.management.commands.services_import.services import ( update_service_counts, update_service_node_counts, ) -from services.models import Service, ServiceNode, Unit, UnitServiceDetails -from smbackend_turku.importers.constants import ( - BIKE_SERVICE_STATION_SERVICE_NAME, - BIKE_SERVICE_STATION_SERVICE_NAMES, - BIKE_SERVICE_STATION_SERVICE_NODE_NAME, - BIKE_SERVICE_STATION_SERVICE_NODE_NAMES, -) -from smbackend_turku.importers.utils import ( - create_service, - create_service_node, - delete_external_source, - get_municipality, - set_field, - set_service_names_field, - set_syncher_object_field, - set_tku_translated_field, - UTC_TIMEZONE, -) +from smbackend_turku.importers.utils import BaseExternalSource -class BikeServiceStationImporter: - - SERVICE_ID = settings.BIKE_SERVICE_STATIONS_IDS["service"] - SERVICE_NODE_ID = settings.BIKE_SERVICE_STATIONS_IDS["service_node"] - UNITS_ID_OFFSET = settings.BIKE_SERVICE_STATIONS_IDS["units_offset"] - SERVICE_NAME = BIKE_SERVICE_STATION_SERVICE_NAME - SERVICE_NAMES = BIKE_SERVICE_STATION_SERVICE_NAMES - SERVICE_NODE_NAME = BIKE_SERVICE_STATION_SERVICE_NODE_NAME - SERVICE_NODE_NAMES = BIKE_SERVICE_STATION_SERVICE_NODE_NAMES - - def __init__(self, logger=None, root_service_node_name=None, test_data=None): +class BikeServiceStationImporter(BaseExternalSource): + def __init__( + self, config=None, logger=None, root_service_node_name=None, test_data=None + ): + super().__init__(config) self.logger = logger self.root_service_node_name = root_service_node_name self.test_data = test_data def import_bike_service_stations(self): - service_id = self.SERVICE_ID self.logger.info("Importing Bike service stations...") content_type = create_bike_service_station_content_type() - saved_bike_service_stations = 0 filtered_objects = get_bike_service_station_objects(geojson_file=self.test_data) - for i, data_obj in enumerate(filtered_objects): - unit_id = i + self.UNITS_ID_OFFSET - obj = Unit(id=unit_id) - set_field(obj, "location", data_obj.geometry) - set_tku_translated_field(obj, "name", data_obj.name) - set_tku_translated_field(obj, "street_address", data_obj.address) - set_tku_translated_field(obj, "description", data_obj.description) - set_field(obj, "extra", data_obj.extra) - # 1 = self produced - set_syncher_object_field(obj, "provider_type", 1) - try: - service = Service.objects.get(id=service_id) - except Service.DoesNotExist: - self.logger.warning('Service "{}" does not exist!'.format(service_id)) - continue - UnitServiceDetails.objects.get_or_create(unit=obj, service=service) - service_nodes = ServiceNode.objects.filter(related_services=service) - obj.service_nodes.add(*service_nodes) - set_field(obj, "root_service_nodes", obj.get_root_service_nodes()[0]) - municipality = get_municipality(data_obj.municipality) - set_field(obj, "municipality", municipality) - set_field(obj, "address_zip", data_obj.zip_code) - - obj.last_modified_time = datetime.now(UTC_TIMEZONE) - set_service_names_field(obj) - obj.save() - create_mobile_unit_as_unit_reference(unit_id, content_type) - saved_bike_service_stations += 1 - update_service_node_counts() - update_service_counts() - self.logger.info(f"Imported {len(filtered_objects)} bike service stations...") + super().save_objects_as_units(filtered_objects, content_type) def delete_bike_service_stations(**kwargs): importer = BikeServiceStationImporter(**kwargs) - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_bike_service_stations, - ) + importer.delete_external_source() update_service_node_counts() update_service_counts() def import_bike_service_stations(**kwargs): importer = BikeServiceStationImporter(**kwargs) - # Delete all Bike service station units before storing, to ensure stored data is up to date. - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_bike_service_stations, - ) - create_service_node( - importer.SERVICE_NODE_ID, - importer.SERVICE_NODE_NAME, - importer.root_service_node_name, - importer.SERVICE_NODE_NAMES, - ) - create_service( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - importer.SERVICE_NAME, - importer.SERVICE_NAMES, - ) importer.import_bike_service_stations() From 28c7208bc428d70bb172a7c7b59763cb271b459f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 30 Jan 2023 16:11:35 +0200 Subject: [PATCH 046/188] Add config for bike service stations --- .../importers/data/external_units_config.yml | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 smbackend_turku/importers/data/external_units_config.yml diff --git a/smbackend_turku/importers/data/external_units_config.yml b/smbackend_turku/importers/data/external_units_config.yml new file mode 100644 index 000000000..b46e22f4d --- /dev/null +++ b/smbackend_turku/importers/data/external_units_config.yml @@ -0,0 +1,24 @@ +external_data_sources: + - name: bike_service_stations + importer_name: BikeServiceStationImporter + root_service_node_name: Vapaa-aika + units_offset: 500000 + # 1 = self produced + provider_type: 1 + service: + id: 500000 + name: + fi: Pyöränkorjauspiste + sv: Cykelservicestation + en: Bike service station + service_node: + id: 500000 + name: + fi: Pyöränkorjauspisteet + sv: Cykelservicestationer + en: Bike service stations + create_mobile_unit_as_unit_reference: True + # For deleting + mobility_data_content_type_name: BikeServiceStation + + From 9d0638984b2d699020bb63018f45958400c4defe Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 30 Jan 2023 16:34:02 +0200 Subject: [PATCH 047/188] Use config yaml config --- .../tests/test_bike_service_stations.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/smbackend_turku/tests/test_bike_service_stations.py b/smbackend_turku/tests/test_bike_service_stations.py index 266c1905f..e43847e8b 100644 --- a/smbackend_turku/tests/test_bike_service_stations.py +++ b/smbackend_turku/tests/test_bike_service_stations.py @@ -7,10 +7,7 @@ from services.models import Service, ServiceNode, Unit from smbackend_turku.importers.bike_service_stations import import_bike_service_stations -from smbackend_turku.importers.constants import ( - BIKE_SERVICE_STATION_SERVICE_NAMES, - BIKE_SERVICE_STATION_SERVICE_NODE_NAMES, -) +from smbackend_turku.importers.utils import get_external_source_config @pytest.mark.django_db @@ -22,26 +19,25 @@ def test_bike_service_stations_import( ): logger = logging.getLogger(__name__) utc_timezone = pytz.timezone("UTC") + config = get_external_source_config("bike_service_stations") # create root servicenode to which the imported service_node will connect ServiceNode.objects.create( - id=42, name="TestRoot", last_modified_time=datetime.now(utc_timezone) + id=42, name="Vapaa-aika", last_modified_time=datetime.now(utc_timezone) ) import_bike_service_stations( logger=logger, - root_service_node_name="TestRoot", + config=config, test_data="bike_service_stations.geojson", ) assert Unit.objects.all().count() == 3 Service.objects.all().count() == 1 service = Service.objects.all()[0] - assert service.name == BIKE_SERVICE_STATION_SERVICE_NAMES["fi"] - assert service.name_sv == BIKE_SERVICE_STATION_SERVICE_NAMES["sv"] - assert service.name_en == BIKE_SERVICE_STATION_SERVICE_NAMES["en"] - service_node = ServiceNode.objects.get( - name=BIKE_SERVICE_STATION_SERVICE_NODE_NAMES["fi"] - ) - assert service_node.name_sv == BIKE_SERVICE_STATION_SERVICE_NODE_NAMES["sv"] - assert service_node.name_en == BIKE_SERVICE_STATION_SERVICE_NODE_NAMES["en"] + assert service.name == config["service"]["name"]["fi"] + assert service.name_sv == config["service"]["name"]["sv"] + assert service.name_en == config["service"]["name"]["en"] + service_node = ServiceNode.objects.get(name=config["service_node"]["name"]["fi"]) + assert service_node.name_sv == config["service_node"]["name"]["sv"] + assert service_node.name_en == config["service_node"]["name"]["en"] nauvo = Unit.objects.get(name="Nauvo") assert nauvo.name_sv == "Nagu" assert nauvo.name_en == "Nauvo" From 45460cc5d8e94fb89c79be5c9e78e91eec9ec88e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:14:28 +0200 Subject: [PATCH 048/188] Make BikeServiceStation class uniform with BaseExternalSource --- mobility_data/importers/bike_service_stations.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mobility_data/importers/bike_service_stations.py b/mobility_data/importers/bike_service_stations.py index b624def20..ce0f10acc 100644 --- a/mobility_data/importers/bike_service_stations.py +++ b/mobility_data/importers/bike_service_stations.py @@ -29,8 +29,7 @@ def __init__(self, feature): self.address = {} self.description = {} self.extra = {} - self.address = {} - self.zip_code = None + self.address_zip = None self.municipality = None self.geometry = GEOSGeometry(feature.geom.wkt, srid=SOURCE_DATA_SRID) self.geometry.transform(settings.DEFAULT_SRID) @@ -41,7 +40,9 @@ def __init__(self, feature): # Addresses are in format: # Uudenmaankatu 18, 20700 Turku / Nylandsgatan 18, 20700 Turku addresses = feature["Osoite"].as_string().split("/") - self.zip_code, self.municipality = addresses[0].split(",")[1].strip().split(" ") + self.address_zip, self.municipality = ( + addresses[0].split(",")[1].strip().split(" ") + ) # remove zip code and municipality addresses = [address.split(",")[0].strip() for address in addresses] for i, language in enumerate(LANGUAGES): @@ -109,7 +110,10 @@ def save_to_database(objects, delete_tables=True): content_type = create_bike_service_station_content_type() for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, geometry=object.geometry + content_type=content_type, + extra=object.extra, + geometry=object.geometry, + address_zip=object.address_zip, ) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "description", object.description) From a5aed9d75168dd8694feefe6cf9e55adbd79a5af Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:15:26 +0200 Subject: [PATCH 049/188] Remove unnecessary assignment --- mobility_data/importers/disabled_and_no_staff_parking.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobility_data/importers/disabled_and_no_staff_parking.py b/mobility_data/importers/disabled_and_no_staff_parking.py index 4c68e89cf..90204a546 100644 --- a/mobility_data/importers/disabled_and_no_staff_parking.py +++ b/mobility_data/importers/disabled_and_no_staff_parking.py @@ -98,8 +98,7 @@ def __init__(self, feature): feature["osoite"].as_string().split("/")[0].strip().split(" ")[-2:] ) try: - municipality = Municipality.objects.get(name=municipality) - self.municipality = municipality + self.municipality = Municipality.objects.get(name=municipality) except Municipality.DoesNotExist: self.municipality = None From 55edea51f30aaddd9bd4d12d1024ec2001f7511a Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:16:15 +0200 Subject: [PATCH 050/188] Make class GasFillingStation uniform with BaseExternalSource --- .../importers/gas_filling_station.py | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/mobility_data/importers/gas_filling_station.py b/mobility_data/importers/gas_filling_station.py index 18481916c..c00fda744 100644 --- a/mobility_data/importers/gas_filling_station.py +++ b/mobility_data/importers/gas_filling_station.py @@ -3,6 +3,7 @@ from django import db from django.conf import settings from django.contrib.gis.geos import Point, Polygon +from munigeo.models import Municipality from mobility_data.models import MobileUnit @@ -30,35 +31,38 @@ class GasFillingStation: def __init__(self, elem, srid=settings.DEFAULT_SRID): # Contains the complete address with zip_code and city self.address = {} + self.extra = {} + self.name = {} # Contains Only steet_name and number self.street_address = {} self.is_active = True attributes = elem.get("attributes") x = attributes.get("LON", 0) y = attributes.get("LAT", 0) - self.point = Point(x, y, srid=srid) - self.name = attributes.get("STATION_NAME", "") + self.geometry = Point(x, y, srid=srid) + self.name["fi"] = attributes.get("STATION_NAME", "") address_field = attributes.get("ADDRESS", "") street_name, street_number = get_street_name_and_number(address_field) - self.zip_code = attributes.get("ZIP_CODE", "") - self.city = attributes.get("CITY", "") - translated_street_names = get_street_name_translations(street_name, self.city) + self.address_zip = attributes.get("ZIP_CODE", "") + municipality_name = attributes.get("CITY", "") + translated_street_names = get_street_name_translations( + street_name, municipality_name + ) + try: + self.municipality = Municipality.objects.get(name=municipality_name) + except Municipality.DoesNotExist: + self.municipality = None + for lang in LANGUAGES: if street_number: - self.address[ - lang - ] = f"{translated_street_names[lang]} {street_number}, " - self.address[lang] += f"{self.zip_code} {self.city}" - self.street_address[ - lang - ] = f"{translated_street_names[lang]} {street_number}" + self.address[lang] = f"{translated_street_names[lang]} {street_number}" else: - self.address[lang] = f"{translated_street_names[lang]}, " - self.address[lang] += f"{self.zip_code} {self.city}" - self.street_address[lang] = f"{translated_street_names[lang]}" + self.address[lang] = f"{translated_street_names[lang]}" - self.operator = attributes.get("OPERATOR", "") + self.operator = attributes.get("OPERATOR", "") self.lng_cng = attributes.get("LNG_CNG", "") + self.extra["operator"] = self.operator + self.extra["lng_cng"] = self.lng_cng def get_filtered_gas_filling_station_objects(json_data=None): @@ -78,7 +82,7 @@ def get_filtered_gas_filling_station_objects(json_data=None): # Filter objects by their location # Polygon used the detect if point intersects. i.e. is in the boundaries of SouthWest Finland. polygon = Polygon(SOUTHWEST_FINLAND_BOUNDARY, srid=SOUTHWEST_FINLAND_BOUNDARY_SRID) - filtered_objects = [o for o in objects if polygon.intersects(o.point)] + filtered_objects = [o for o in objects if polygon.intersects(o.geometry)] logger.info( "Filtered: {} gas filling stations by location to: {}.".format( len(json_data["features"]), len(filtered_objects) @@ -107,18 +111,15 @@ def save_to_database(objects, delete_tables=True): content_type = create_gas_filling_station_content_type() for object in objects: is_active = object.is_active - name = object.name - extra = {} - extra["operator"] = object.operator - extra["lng_cng"] = object.lng_cng - mobile_unit = MobileUnit.objects.create( is_active=is_active, - name=name, - geometry=object.point, - extra=extra, + geometry=object.geometry, + extra=object.extra, content_type=content_type, + address_zip=object.address_zip, + municipality=object.municipality, ) + set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() logger.info(f"Saved {len(objects)} gas filling stations to database.") From 3f93ff099cdc0c782ae7154f2b3cbbf2c5dc5afb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:17:03 +0200 Subject: [PATCH 051/188] Add addresszip and municipality tests. --- mobility_data/tests/test_import_gas_filling_stations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobility_data/tests/test_import_gas_filling_stations.py b/mobility_data/tests/test_import_gas_filling_stations.py index 0a6ee2fa1..960ef579f 100644 --- a/mobility_data/tests/test_import_gas_filling_stations.py +++ b/mobility_data/tests/test_import_gas_filling_stations.py @@ -7,7 +7,7 @@ @pytest.mark.django_db -def test_importer(): +def test_importer(municipalities): import_command("import_gas_filling_stations", test_mode="gas_filling_stations.json") assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 @@ -15,7 +15,9 @@ def test_importer(): assert MobileUnit.objects.get(name="Raisio Kuninkoja") unit = MobileUnit.objects.get(name="Turku Satama") assert unit - assert unit.address == "Tuontiväylä 42 abc 1-2, 20200 Turku" + assert unit.address == "Tuontiväylä 42 abc 1-2" + assert unit.address_zip == "20200" + assert unit.municipality.name == "Turku" # Transform to source data srid unit.geometry.transform(3857) assert pytest.approx(unit.geometry.x, 0.0000000001) == 2472735.3962113541 From 13e77d2d82cfc31b7a65f9dcbd115602429c4b4f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:18:03 +0200 Subject: [PATCH 052/188] Remove useless assertion --- mobility_data/tests/test_import_gas_filling_stations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mobility_data/tests/test_import_gas_filling_stations.py b/mobility_data/tests/test_import_gas_filling_stations.py index 960ef579f..276ba606d 100644 --- a/mobility_data/tests/test_import_gas_filling_stations.py +++ b/mobility_data/tests/test_import_gas_filling_stations.py @@ -14,7 +14,6 @@ def test_importer(municipalities): assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 2 assert MobileUnit.objects.get(name="Raisio Kuninkoja") unit = MobileUnit.objects.get(name="Turku Satama") - assert unit assert unit.address == "Tuontiväylä 42 abc 1-2" assert unit.address_zip == "20200" assert unit.municipality.name == "Turku" From 5d5be2c514a95a4faf0e57b5d0a00e69d855e65c Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:19:06 +0200 Subject: [PATCH 053/188] Add gas filling stations --- .../importers/data/external_units_config.yml | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/smbackend_turku/importers/data/external_units_config.yml b/smbackend_turku/importers/data/external_units_config.yml index b46e22f4d..27532025a 100644 --- a/smbackend_turku/importers/data/external_units_config.yml +++ b/smbackend_turku/importers/data/external_units_config.yml @@ -21,4 +21,23 @@ external_data_sources: # For deleting mobility_data_content_type_name: BikeServiceStation - + - name: gas_filling_stations + importer_name: GasFillingStationImporter + root_service_node_name: Vapaa-aika + units_offset: 300000 + service: + id: 300000 + name: + fi: Kaasutankkausasema + sv: Tankstation med gas + en: Gas filling station + service_node: + id: 300000 + name: + fi: Kaasutankkausasemat + sv: Tankstationer med gas + en: Gas filling stations + create_mobile_unit_as_unit_reference: True + # For deleting + mobility_data_content_type_name: GasFillingStation + From e3ad76ac9f920df0604ae36226676760de4f0949 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:22:37 +0200 Subject: [PATCH 054/188] Use config from yaml file --- .../tests/test_gas_filling_stations.py | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/smbackend_turku/tests/test_gas_filling_stations.py b/smbackend_turku/tests/test_gas_filling_stations.py index 77d676431..4882818d4 100644 --- a/smbackend_turku/tests/test_gas_filling_stations.py +++ b/smbackend_turku/tests/test_gas_filling_stations.py @@ -3,23 +3,22 @@ import pytest import pytz -from django.conf import settings from services.models import Service, ServiceNode, Unit -from smbackend_turku.importers.stations import ( - GasFillingStationImporter as Importer, - import_gas_filling_stations, -) +from smbackend_turku.importers.stations import import_gas_filling_stations +from smbackend_turku.importers.utils import get_external_source_config from smbackend_turku.tests.utils import create_municipalities, get_test_resource @pytest.mark.django_db def test_gas_filling_stations_import(): logger = logging.getLogger(__name__) + config = get_external_source_config("gas_filling_stations") + utc_timezone = pytz.timezone("UTC") # create root servicenode to which the imported service_node will connect root_service_node = ServiceNode.objects.create( - id=42, name="TestRoot", last_modified_time=datetime.now(utc_timezone) + id=42, name="Vapaa-aika", last_modified_time=datetime.now(utc_timezone) ) # Municipality must be created in order to update_service_node_count() # to execute without errors @@ -27,24 +26,21 @@ def test_gas_filling_stations_import(): # Import using fixture data import_gas_filling_stations( logger=logger, - root_service_node_name="TestRoot", + config=config, test_data=get_test_resource(resource_name="gas_filling_stations"), ) - service = Service.objects.get(name=Importer.SERVICE_NAME) - assert service - assert service.id == settings.GAS_FILLING_STATIONS_IDS["service"] - service_node = ServiceNode.objects.get(name=Importer.SERVICE_NODE_NAME) - assert service_node - assert service_node.id == settings.GAS_FILLING_STATIONS_IDS["service_node"] + service = Service.objects.get(name=config["service"]["name"]["fi"]) + assert service.id == config["service"]["id"] + service_node = ServiceNode.objects.get(name=config["service_node"]["name"]["fi"]) + assert service_node.id == config["service_node"]["id"] assert service_node.parent.id == root_service_node.id assert Unit.objects.all().count() == 2 - assert Unit.objects.all()[1].id == settings.GAS_FILLING_STATIONS_IDS["units_offset"] + assert Unit.objects.all()[1].id == config["units_offset"] assert Unit.objects.get(name="Raisio Kuninkoja") unit = Unit.objects.get(name="Turku Satama") - assert unit assert pytest.approx(unit.location.x, 0.0000000001) == 236760.1062021295 assert unit.extra["operator"] == "Gasum" assert unit.service_nodes.all().count() == 1 assert unit.services.all().count() == 1 - assert unit.services.all()[0].name == Importer.SERVICE_NAME - assert unit.service_nodes.all()[0].name == Importer.SERVICE_NODE_NAME + assert unit.services.all()[0].name == config["service"]["name"]["fi"] + assert unit.service_nodes.all()[0].name == config["service_node"]["name"]["fi"] From 974f7c5db023efd8564adae03a78c8c86ddf0084 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 10:23:28 +0200 Subject: [PATCH 055/188] Remove root_service_node_name param --- smbackend_turku/importers/bike_service_stations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/smbackend_turku/importers/bike_service_stations.py b/smbackend_turku/importers/bike_service_stations.py index d4ea58ec2..1a246ee79 100644 --- a/smbackend_turku/importers/bike_service_stations.py +++ b/smbackend_turku/importers/bike_service_stations.py @@ -10,12 +10,9 @@ class BikeServiceStationImporter(BaseExternalSource): - def __init__( - self, config=None, logger=None, root_service_node_name=None, test_data=None - ): + def __init__(self, config=None, logger=None, test_data=None): super().__init__(config) self.logger = logger - self.root_service_node_name = root_service_node_name self.test_data = test_data def import_bike_service_stations(self): From c66fc2860f5fc9f58af7c925827f3226e008c1c7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 11:53:49 +0200 Subject: [PATCH 056/188] Add chargin stations --- .../importers/data/external_units_config.yml | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/smbackend_turku/importers/data/external_units_config.yml b/smbackend_turku/importers/data/external_units_config.yml index 27532025a..b4bca530f 100644 --- a/smbackend_turku/importers/data/external_units_config.yml +++ b/smbackend_turku/importers/data/external_units_config.yml @@ -1,5 +1,6 @@ external_data_sources: - name: bike_service_stations + # Name of the class importer_name: BikeServiceStationImporter root_service_node_name: Vapaa-aika units_offset: 500000 @@ -17,27 +18,48 @@ external_data_sources: fi: Pyöränkorjauspisteet sv: Cykelservicestationer en: Bike service stations - create_mobile_unit_as_unit_reference: True + # When importing, create mobile_units that has a reference id to the unit. + # The data of the mobile_unit is then serialized from the services_unit table. + create_mobile_units_with_unit_reference: True # For deleting mobility_data_content_type_name: BikeServiceStation - name: gas_filling_stations importer_name: GasFillingStationImporter root_service_node_name: Vapaa-aika - units_offset: 300000 + units_offset: 200000 service: - id: 300000 + id: 200000 name: fi: Kaasutankkausasema sv: Tankstation med gas en: Gas filling station service_node: - id: 300000 + id: 200000 name: fi: Kaasutankkausasemat sv: Tankstationer med gas en: Gas filling stations - create_mobile_unit_as_unit_reference: True + create_mobile_units_with_unit_reference: True # For deleting mobility_data_content_type_name: GasFillingStation + - name: charging_stations + importer_name: ChargingStationImporter + root_service_node_name: Vapaa-aika + units_offset: 300000 + service: + id: 300000 + name: + fi: Autojen sähkölatauspiste + sv: Elladdningsstation för bilar + en: Car e-charging point + service_node: + id: 300000 + name: + fi: Autojen sähkölatauspisteet + sv: Elladdningsstationer för bilar + en: Car e-charging points + create_mobile_units_with_unit_reference: True + # For deleting + mobility_data_content_type_name: ChargingStation \ No newline at end of file From fdb356d9c78c2e1a91cf9b12a77bff3a391207b1 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 12:04:45 +0200 Subject: [PATCH 057/188] Add address_zip --- mobility_data/importers/charging_stations.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobility_data/importers/charging_stations.py b/mobility_data/importers/charging_stations.py index 8d9e1708c..e395c445f 100644 --- a/mobility_data/importers/charging_stations.py +++ b/mobility_data/importers/charging_stations.py @@ -75,7 +75,7 @@ def __init__(self, values): self.extra["other"] = values["other"] self.extra["payment"] = values["payment"] self.municipality = get_municipality_name(self.geometry) - self.zip_code = get_postal_code(self.geometry) + self.address_zip = get_postal_code(self.geometry) tmp = values["address"].split(" ") address_number = None street_name = tmp[0] @@ -184,6 +184,7 @@ def save_to_database(objects, delete_tables=True): geometry=object.geometry, extra=object.extra, content_type=content_type, + address_zip=object.address_zip, ) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) From 6fc477f9ffc809552c527c51ed94b00a747cb17e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 12:05:11 +0200 Subject: [PATCH 058/188] Remove unnecessary assertion --- mobility_data/tests/test_import_charging_stations.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mobility_data/tests/test_import_charging_stations.py b/mobility_data/tests/test_import_charging_stations.py index 97572225f..33826361e 100644 --- a/mobility_data/tests/test_import_charging_stations.py +++ b/mobility_data/tests/test_import_charging_stations.py @@ -21,7 +21,6 @@ def test_import_charging_stations( assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 3 aimopark = MobileUnit.objects.get(name="Aimopark, Yliopistonkatu 29") - assert aimopark assert aimopark.address == "Yliopistonkatu 29" assert aimopark.address_sv == "Universitetsgatan 29" assert aimopark.address_en == "Yliopistonkatu 29" From 916111ed317fd9aaa1e3288d48f99109a86f8eb4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 12:53:03 +0200 Subject: [PATCH 059/188] Use super class BaseExternalSource for importing --- smbackend_turku/importers/stations.py | 224 ++------------------------ 1 file changed, 11 insertions(+), 213 deletions(-) diff --git a/smbackend_turku/importers/stations.py b/smbackend_turku/importers/stations.py index b8025cf44..22f16b141 100644 --- a/smbackend_turku/importers/stations.py +++ b/smbackend_turku/importers/stations.py @@ -1,259 +1,57 @@ -from datetime import datetime - -from django.conf import settings - from mobility_data.importers.charging_stations import ( create_charging_station_content_type, - delete_charging_stations as mobility_data_delete_charging_stations, get_charging_station_objects, ) from mobility_data.importers.gas_filling_station import ( create_gas_filling_station_content_type, - delete_gas_filling_stations as mobility_data_delete_gas_filling_stations, get_filtered_gas_filling_station_objects, ) -from mobility_data.importers.utils import create_mobile_unit_as_unit_reference -from services.management.commands.services_import.services import ( - update_service_counts, - update_service_node_counts, -) -from services.models import Service, ServiceNode, Unit, UnitServiceDetails -from smbackend_turku.importers.utils import ( - create_service, - create_service_node, - delete_external_source, - get_municipality, - set_field, - set_service_names_field, - set_tku_translated_field, - UTC_TIMEZONE, -) - -from .constants import ( - CHARGING_STATION_SERVICE_NAME, - CHARGING_STATION_SERVICE_NAMES, - CHARGING_STATION_SERVICE_NODE_NAME, - CHARGING_STATION_SERVICE_NODE_NAMES, - GAS_FILLING_STATION_SERVICE_NAME, - GAS_FILLING_STATION_SERVICE_NAMES, - GAS_FILLING_STATION_SERVICE_NODE_NAME, - GAS_FILLING_STATION_SERVICE_NODE_NAMES, -) - -LANGUAGES = [language[0] for language in settings.LANGUAGES] -SOURCE_DATA_SRID = 4326 - - -def create_language_dict(value): - """ - Helper function that generates a dict with elements for every language with - the value given as parameter. - :param value: the value to be set for all the languages - :return: the dict - """ - lang_dict = {} - languages = [language[0] for language in settings.LANGUAGES] - for lang in languages: - lang_dict[lang] = value - return lang_dict +from smbackend_turku.importers.utils import BaseExternalSource -# def get_first_available_id(model, offset): -# """ -# Find the highest unit id and add 1. This ensures that we get unique ids. -# :param model: the model class -# :return: the highest available id. -# """ -# queryset = model.objects.all() -# if queryset.count() > 0: -# return model.objects.all().order_by("-id")[0].id+1 -# else: -# # This branch is evaluated only when running tests -# return 100000 - - -class GasFillingStationImporter: - - SERVICE_ID = settings.GAS_FILLING_STATIONS_IDS["service"] - SERVICE_NODE_ID = settings.GAS_FILLING_STATIONS_IDS["service_node"] - UNITS_ID_OFFSET = settings.GAS_FILLING_STATIONS_IDS["units_offset"] - - SERVICE_NODE_NAME = GAS_FILLING_STATION_SERVICE_NODE_NAME - SERVICE_NAME = GAS_FILLING_STATION_SERVICE_NAME - SERVICE_NODE_NAMES = GAS_FILLING_STATION_SERVICE_NODE_NAMES - SERVICE_NAMES = GAS_FILLING_STATION_SERVICE_NAMES - - def __init__(self, logger=None, root_service_node_name=None, test_data=None): +class GasFillingStationImporter(BaseExternalSource): + def __init__(self, config=None, logger=None, test_data=None): + super().__init__(config) self.logger = logger - self.root_service_node_name = root_service_node_name self.test_data = test_data def import_gas_filling_stations(self): - service_id = self.SERVICE_ID self.logger.info("Importing gas filling stations...") content_type = create_gas_filling_station_content_type() filtered_objects = get_filtered_gas_filling_station_objects( json_data=self.test_data ) - for i, data_obj in enumerate(filtered_objects): - unit_id = i + self.UNITS_ID_OFFSET - obj = Unit(id=unit_id) - point = data_obj.point - point.transform(SOURCE_DATA_SRID) - set_field(obj, "location", point) - set_tku_translated_field(obj, "name", create_language_dict(data_obj.name)) - set_tku_translated_field(obj, "street_address", data_obj.street_address) - set_tku_translated_field(obj, "address_postal_full", data_obj.address) - set_field(obj, "address_zip", data_obj.zip_code) - description = "{} {}".format(data_obj.operator, data_obj.lng_cng) - set_tku_translated_field( - obj, "description", create_language_dict(description) - ) - extra = {} - extra["operator"] = data_obj.operator - extra["lng_cng"] = data_obj.lng_cng - set_field(obj, "extra", extra) - try: - service = Service.objects.get(id=service_id) - except Service.DoesNotExist: - self.logger.warning('Service "{}" does not exist!'.format(service_id)) - continue - UnitServiceDetails.objects.get_or_create(unit=obj, service=service) - service_nodes = ServiceNode.objects.filter(related_services=service) - obj.service_nodes.add(*service_nodes) - set_field(obj, "root_service_nodes", obj.get_root_service_nodes()[0]) - municipality = get_municipality(data_obj.city) - set_field(obj, "municipality", municipality) - obj.last_modified_time = datetime.now(UTC_TIMEZONE) - set_service_names_field(obj) - obj.save() - # Save to mobile_units tables as reference to services_unit table. - create_mobile_unit_as_unit_reference(unit_id, content_type) - update_service_node_counts() - update_service_counts() - self.logger.info(f"Imported {len(filtered_objects)} gas filling stations...") - + super().save_objects_as_units(filtered_objects, content_type) -class ChargingStationImporter: - SERVICE_ID = settings.CHARGING_STATIONS_IDS["service"] - SERVICE_NODE_ID = settings.CHARGING_STATIONS_IDS["service_node"] - UNITS_ID_OFFSET = settings.CHARGING_STATIONS_IDS["units_offset"] - SERVICE_NODE_NAME = CHARGING_STATION_SERVICE_NODE_NAME - SERVICE_NAME = CHARGING_STATION_SERVICE_NAME - SERVICE_NODE_NAMES = CHARGING_STATION_SERVICE_NODE_NAMES - SERVICE_NAMES = CHARGING_STATION_SERVICE_NAMES - - def __init__( - self, logger=None, importer=None, root_service_node_name=None, test_data=None - ): +class ChargingStationImporter(BaseExternalSource): + def __init__(self, logger=None, config=None, importer=None, test_data=None): + super().__init__(config) self.logger = logger - self.importer = importer - self.root_service_node_name = root_service_node_name self.test_data = test_data def import_charging_stations(self): self.logger.info("Importing charging stations...") - service_id = self.SERVICE_ID filtered_objects = get_charging_station_objects(csv_file=self.test_data) - # create mobility_data content type content_type = create_charging_station_content_type() - for i, data_obj in enumerate(filtered_objects): - unit_id = i + self.UNITS_ID_OFFSET - obj = Unit(id=unit_id) - geometry = data_obj.geometry - geometry.transform(SOURCE_DATA_SRID) - set_field(obj, "location", data_obj.geometry) - set_field(obj, "address_zip", data_obj.zip_code) - set_tku_translated_field(obj, "name", data_obj.name) - set_tku_translated_field(obj, "street_address", data_obj.address) - set_tku_translated_field(obj, "description", CHARGING_STATION_SERVICE_NAMES) - set_field(obj, "extra", data_obj.extra) - try: - service = Service.objects.get(id=service_id) - except Service.DoesNotExist: - self.logger.warning('Service "{}" does not exist!'.format(service_id)) - continue - UnitServiceDetails.objects.get_or_create(unit=obj, service=service) - service_nodes = ServiceNode.objects.filter(related_services=service) - obj.service_nodes.add(*service_nodes) - set_field(obj, "root_service_nodes", obj.get_root_service_nodes()[0]) - municipality = get_municipality(data_obj.municipality) - set_field(obj, "municipality", municipality) - obj.last_modified_time = datetime.now(UTC_TIMEZONE) - set_service_names_field(obj) - obj.save() - # Save to mobile_units tables as reference to services_unit table. - create_mobile_unit_as_unit_reference(unit_id, content_type) - update_service_node_counts() - update_service_counts() - self.logger.info(f"Imported {len(filtered_objects)} charging stations...") + super().save_objects_as_units(filtered_objects, content_type) def delete_gas_filling_stations(**kwargs): importer = GasFillingStationImporter(**kwargs) - # Delete all gas filling station units before storing, to ensure stored data is up-to-date. - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_gas_filling_stations, - ) - update_service_node_counts() - update_service_counts() + importer.delete_external_source() def import_gas_filling_stations(**kwargs): importer = GasFillingStationImporter(**kwargs) - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_gas_filling_stations, - ) - - create_service_node( - importer.SERVICE_NODE_ID, - importer.SERVICE_NODE_NAME, - importer.root_service_node_name, - importer.SERVICE_NODE_NAMES, - ) - create_service( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - importer.SERVICE_NAME, - importer.SERVICE_NAMES, - ) importer.import_gas_filling_stations() def delete_charging_stations(**kwargs): importer = ChargingStationImporter(**kwargs) - # Delete all charging station units before storing, to ensure stored data is up-to-date. - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_charging_stations, - ) - update_service_node_counts() - update_service_counts() + importer.delete_external_source() def import_charging_stations(**kwargs): importer = ChargingStationImporter(**kwargs) - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_charging_stations, - ) - create_service_node( - importer.SERVICE_NODE_ID, - importer.SERVICE_NODE_NAME, - importer.root_service_node_name, - importer.SERVICE_NODE_NAMES, - ) - create_service( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - importer.SERVICE_NAME, - importer.SERVICE_NAMES, - ) importer.import_charging_stations() From df20e489a231b485606ffda2f16004702f23d05b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 12:54:00 +0200 Subject: [PATCH 060/188] Remove service node counts, moved to util function --- smbackend_turku/importers/bike_service_stations.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/smbackend_turku/importers/bike_service_stations.py b/smbackend_turku/importers/bike_service_stations.py index 1a246ee79..ad468fc41 100644 --- a/smbackend_turku/importers/bike_service_stations.py +++ b/smbackend_turku/importers/bike_service_stations.py @@ -2,10 +2,6 @@ create_bike_service_station_content_type, get_bike_service_station_objects, ) -from services.management.commands.services_import.services import ( - update_service_counts, - update_service_node_counts, -) from smbackend_turku.importers.utils import BaseExternalSource @@ -25,8 +21,6 @@ def import_bike_service_stations(self): def delete_bike_service_stations(**kwargs): importer = BikeServiceStationImporter(**kwargs) importer.delete_external_source() - update_service_node_counts() - update_service_counts() def import_bike_service_stations(**kwargs): From 958a823148fea315248253ca3d8c96ab64d60278 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 13:03:14 +0200 Subject: [PATCH 061/188] Use config --- .../tests/test_charging_stations.py | 27 ++++++++----------- 1 file changed, 11 insertions(+), 16 deletions(-) diff --git a/smbackend_turku/tests/test_charging_stations.py b/smbackend_turku/tests/test_charging_stations.py index 6508f2670..459e2ecf9 100644 --- a/smbackend_turku/tests/test_charging_stations.py +++ b/smbackend_turku/tests/test_charging_stations.py @@ -5,11 +5,8 @@ import pytz from services.models import Service, ServiceNode, Unit -from smbackend_turku.importers.constants import ( - CHARGING_STATION_SERVICE_NAMES, - CHARGING_STATION_SERVICE_NODE_NAMES, -) from smbackend_turku.importers.stations import import_charging_stations +from smbackend_turku.importers.utils import get_external_source_config @pytest.mark.django_db @@ -24,34 +21,32 @@ def test_charging_stations_import( ): logger = logging.getLogger(__name__) utc_timezone = pytz.timezone("UTC") + config = get_external_source_config("gas_filling_stations") + # create root servicenode to which the imported service_node will connect ServiceNode.objects.create( - id=42, name="TestRoot", last_modified_time=datetime.now(utc_timezone) + id=42, name="Vapaa-aika", last_modified_time=datetime.now(utc_timezone) ) - import_charging_stations( logger=logger, - root_service_node_name="TestRoot", + config=config, test_data="charging_stations.csv", ) assert Unit.objects.all().count() == 3 Service.objects.all().count() == 1 service = Service.objects.all()[0] - assert service.name == CHARGING_STATION_SERVICE_NAMES["fi"] - assert service.name_sv == CHARGING_STATION_SERVICE_NAMES["sv"] - assert service.name_en == CHARGING_STATION_SERVICE_NAMES["en"] - service_node = ServiceNode.objects.get( - name=CHARGING_STATION_SERVICE_NODE_NAMES["fi"] - ) - assert service_node.name_sv == CHARGING_STATION_SERVICE_NODE_NAMES["sv"] - assert service_node.name_en == CHARGING_STATION_SERVICE_NODE_NAMES["en"] + assert service.name == config["service"]["name"]["fi"] + assert service.name_sv == config["service"]["name"]["sv"] + assert service.name_en == config["service"]["name"]["en"] + service_node = ServiceNode.objects.get(name=config["service_node"]["name"]["fi"]) + assert service_node.name_sv == config["service_node"]["name"]["sv"] + assert service_node.name_en == config["service_node"]["name"]["en"] aimopark = Unit.objects.get(name="Aimopark, Yliopistonkatu 29") assert aimopark.name_sv == "Aimopark, Universitetsgatan 29" assert aimopark.street_address == "Yliopistonkatu 29" assert aimopark.street_address_sv == "Universitetsgatan 29" assert aimopark.municipality.name == "Turku" assert aimopark.address_zip == "20100" - assert aimopark.description_sv == CHARGING_STATION_SERVICE_NAMES["sv"] assert aimopark.root_service_nodes == "42" assert aimopark.services.count() == 1 assert aimopark.services.all()[0] == service From e0afaa727d2b5e20f89a4fb0c039021c684d671f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:49:51 +0200 Subject: [PATCH 062/188] Add bicycle stands --- .../importers/data/external_units_config.yml | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/smbackend_turku/importers/data/external_units_config.yml b/smbackend_turku/importers/data/external_units_config.yml index b4bca530f..78c48a58f 100644 --- a/smbackend_turku/importers/data/external_units_config.yml +++ b/smbackend_turku/importers/data/external_units_config.yml @@ -62,4 +62,24 @@ external_data_sources: en: Car e-charging points create_mobile_units_with_unit_reference: True # For deleting - mobility_data_content_type_name: ChargingStation \ No newline at end of file + mobility_data_content_type_name: ChargingStation + + - name: bicycle_stands + importer_name: BicycleStandsImporter + root_service_node_name: Vapaa-aika + units_offset: 400000 + service: + id: 400000 + name: + fi: Pyöräpysäköinti + sv: Cykelparkering + en: Bicycle parking + service_node: + id: 400000 + name: + fi: Pyöräpysäköinnit + sv: Cykelparkeringar + en: Bicycle parkings + create_mobile_units_with_unit_reference: True + # For deleting + mobility_data_content_type_name: BicycleStand \ No newline at end of file From d0aa4eec00de16634c202c8ace4dc2dc7b3a1226 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:50:44 +0200 Subject: [PATCH 063/188] Read external sources from yaml config --- .../commands/turku_services_import.py | 74 ++++++------------- 1 file changed, 23 insertions(+), 51 deletions(-) diff --git a/smbackend_turku/management/commands/turku_services_import.py b/smbackend_turku/management/commands/turku_services_import.py index a66ac3d68..a9d324523 100644 --- a/smbackend_turku/management/commands/turku_services_import.py +++ b/smbackend_turku/management/commands/turku_services_import.py @@ -9,11 +9,11 @@ from smbackend_turku.importers.accessibility import import_accessibility from smbackend_turku.importers.addresses import import_addresses -from smbackend_turku.importers.bicycle_stands import ( +from smbackend_turku.importers.bicycle_stands import ( # noqa: F401 delete_bicycle_stands, import_bicycle_stands, ) -from smbackend_turku.importers.bike_service_stations import ( +from smbackend_turku.importers.bike_service_stations import ( # noqa: F401 delete_bike_service_stations, import_bike_service_stations, ) @@ -23,13 +23,15 @@ import_geo_search_addresses, ) from smbackend_turku.importers.services import import_services -from smbackend_turku.importers.stations import ( +from smbackend_turku.importers.stations import ( # noqa: F401 delete_charging_stations, delete_gas_filling_stations, import_charging_stations, import_gas_filling_stations, ) from smbackend_turku.importers.units import import_units +from smbackend_turku.importers.utils import get_external_source_config # noqa: F401 +from smbackend_turku.importers.utils import get_configured_external_sources_names class Command(BaseCommand): @@ -38,13 +40,7 @@ class Command(BaseCommand): # Umbrella source that imports all external_sources MOBILITY_DATA = "mobility_data" - external_sources = [ - "gas_filling_stations", - "charging_stations", - "bicycle_stands", - "bike_service_stations", - ] - + external_sources = get_configured_external_sources_names() importer_types = [ "services", "accessibility", @@ -58,6 +54,21 @@ class Command(BaseCommand): supported_languages = [lang[0] for lang in settings.LANGUAGES] + for name in external_sources: + code = f""" +@db.transaction.atomic +def import_{name}(self): + config = get_external_source_config("{name}") + import_{name}(logger=self.logger, config=config) + print('IMPORT') +@db.transaction.atomic +def delete_{name}(self): + config = get_external_source_config("{name}") + delete_{name}(logger=self.logger, config=config) + print('DELETE') + """ + exec(code) + def __init__(self): super(Command, self).__init__() for imp in self.importer_types: @@ -135,49 +146,9 @@ def import_enriched_addresses(self): def import_divisions(self): return import_divisions(logger=self.logger) - @db.transaction.atomic - def import_gas_filling_stations(self): - import_gas_filling_stations( - logger=self.logger, root_service_node_name="Vapaa-aika" - ) - - @db.transaction.atomic - def delete_gas_filling_stations(self): - delete_gas_filling_stations(logger=self.logger) - - @db.transaction.atomic - def import_charging_stations(self): - import_charging_stations( - logger=self.logger, root_service_node_name="Vapaa-aika" - ) - - @db.transaction.atomic - def delete_charging_stations(self): - delete_charging_stations(logger=self.logger) - - @db.transaction.atomic - def import_bicycle_stands(self): - import_bicycle_stands( - logger=self.logger, - root_service_node_name="Vapaa-aika", - ) - - @db.transaction.atomic - def delete_bicycle_stands(self): - delete_bicycle_stands(logger=self.logger) - - @db.transaction.atomic - def delete_bike_service_stations(self): - delete_bike_service_stations(logger=self.logger) - - @db.transaction.atomic - def import_bike_service_stations(self): - import_bike_service_stations( - logger=self.logger, root_service_node_name="Vapaa-aika" - ) - @db.transaction.atomic def import_mobility_data(self): + # TODO fix this self.import_bicycle_stands() self.import_gas_filling_stations() @@ -185,6 +156,7 @@ def import_mobility_data(self): # to make sure translated fields are populated correctly. @translation.override(settings.LANGUAGES[0][0]) def handle(self, **options): + self.options = options self.verbosity = int(options.get("verbosity", 1)) self.logger = logging.getLogger("turku_services_import") From ae2820291aa24ff25bfa00fb9575017e47f78819 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:51:28 +0200 Subject: [PATCH 064/188] Add check for attrs --- smbackend_turku/importers/utils.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/smbackend_turku/importers/utils.py b/smbackend_turku/importers/utils.py index 7ca383ea9..2d0290677 100644 --- a/smbackend_turku/importers/utils.py +++ b/smbackend_turku/importers/utils.py @@ -7,6 +7,7 @@ import pytz import requests import yaml +from django import db from django.conf import settings from django.core.exceptions import ObjectDoesNotExist from munigeo.models import ( @@ -345,6 +346,7 @@ def create_service(service_id, service_node_id, service_name, service_names): service.save() +@db.transaction.atomic def delete_external_source( service_id, service_node_id, @@ -357,6 +359,8 @@ def delete_external_source( Service.objects.filter(id=service_id).delete() ServiceNode.objects.filter(id=service_node_id).delete() delete_mobile_units(mobile_units_content_type_name) + update_service_node_counts() + update_service_counts() class BaseExternalSource: @@ -390,6 +394,7 @@ def delete_external_source(self): self.config["mobility_data_content_type_name"], ) + @db.transaction.atomic def save_objects_as_units(self, objects, content_type): for i, object in enumerate(objects): unit_id = i + self.UNITS_ID_OFFSET @@ -397,8 +402,10 @@ def save_objects_as_units(self, objects, content_type): set_field(unit, "location", object.geometry) set_tku_translated_field(unit, "name", object.name) set_tku_translated_field(unit, "street_address", object.address) - set_tku_translated_field(unit, "description", object.description) - set_field(unit, "extra", object.extra) + if hasattr(object, "description"): + set_tku_translated_field(unit, "description", object.description) + if hasattr(object, "extra"): + set_field(unit, "extra", object.extra) if "provider_type" in self.config: set_syncher_object_field( unit, "provider_type", self.config["provider_type"] @@ -414,13 +421,16 @@ def save_objects_as_units(self, objects, content_type): service_nodes = ServiceNode.objects.filter(related_services=service) unit.service_nodes.add(*service_nodes) set_field(unit, "root_service_nodes", unit.get_root_service_nodes()[0]) - municipality = get_municipality(object.municipality) - set_field(unit, "municipality", municipality) - set_field(unit, "address_zip", object.zip_code) + if hasattr(object, "municipality"): + municipality = get_municipality(object.municipality) + set_field(unit, "municipality", municipality) + + if hasattr(object, "address_zip"): + set_field(unit, "address_zip", object.address_zip) unit.last_modified_time = datetime.datetime.now(UTC_TIMEZONE) set_service_names_field(unit) unit.save() - if self.config.get("create_mobile_unit_as_unit_reference", False): + if self.config.get("create_mobile_units_with_unit_reference", False): create_mobile_unit_as_unit_reference(unit_id, content_type) update_service_node_counts() update_service_counts() From 844110a9c28eaecbfb2ddebb81a0a74f3fe7e639 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:52:06 +0200 Subject: [PATCH 065/188] Read external units using yaml config --- smbackend_turku/importers/units.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/smbackend_turku/importers/units.py b/smbackend_turku/importers/units.py index 7ea00056d..1b35eb62c 100644 --- a/smbackend_turku/importers/units.py +++ b/smbackend_turku/importers/units.py @@ -24,8 +24,8 @@ UnitServiceDetails, ) from services.utils import AccessibilityShortcomingCalculator -from smbackend_turku.importers.services import EXTERNAL_IMPORTERS from smbackend_turku.importers.utils import ( + get_external_sources_yaml_config, get_localized_value, get_municipality, get_municipality_name_by_point, @@ -114,11 +114,10 @@ def import_units(self): for unit in units: self._handle_unit(unit) if not self.delete_external_source: - for importer in EXTERNAL_IMPORTERS: - self._handle_external_units(importer) + for config in get_external_sources_yaml_config(): + self._handle_external_units(config) self.unitsyncher.finish() - update_service_node_counts() update_service_counts() remove_empty_service_nodes(self.logger) @@ -153,15 +152,15 @@ def _handle_unit(self, unit_data): self._save_object(obj) self.unitsyncher.mark(obj) - def _handle_external_units(self, importer): + def _handle_external_units(self, config): """ Mark units that has been imported from external source. If not marked the unitsyncher.finish() will delete the units. """ - + # breakpoint() service = None try: - service = Service.objects.get(name=importer.SERVICE_NAME) + service = Service.objects.get(name=config["service"]["name"]["fi"]) except Service.DoesNotExist: pass if service: From f82aa1e06f34603a90a98b3ea9ea24a4f70f4b7d Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:53:26 +0200 Subject: [PATCH 066/188] Remove comment --- smbackend_turku/importers/units.py | 1 - 1 file changed, 1 deletion(-) diff --git a/smbackend_turku/importers/units.py b/smbackend_turku/importers/units.py index 1b35eb62c..d854fedd1 100644 --- a/smbackend_turku/importers/units.py +++ b/smbackend_turku/importers/units.py @@ -157,7 +157,6 @@ def _handle_external_units(self, config): Mark units that has been imported from external source. If not marked the unitsyncher.finish() will delete the units. """ - # breakpoint() service = None try: service = Service.objects.get(name=config["service"]["name"]["fi"]) From 08e8bb11dec6f3de80b7ae899159f8e769631d84 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:54:07 +0200 Subject: [PATCH 067/188] Read externally imported services via yaml config file --- smbackend_turku/importers/services.py | 36 ++++++++------------------- 1 file changed, 11 insertions(+), 25 deletions(-) diff --git a/smbackend_turku/importers/services.py b/smbackend_turku/importers/services.py index ff369bdb9..f85480386 100644 --- a/smbackend_turku/importers/services.py +++ b/smbackend_turku/importers/services.py @@ -8,14 +8,9 @@ update_service_root_service_nodes, ) from services.models import Service, ServiceNode -from smbackend_turku.importers.bicycle_stands import BicycleStandImporter -from smbackend_turku.importers.bike_service_stations import BikeServiceStationImporter -from smbackend_turku.importers.stations import ( - ChargingStationImporter, - GasFillingStationImporter, -) from smbackend_turku.importers.utils import ( convert_code_to_int, + get_external_sources_yaml_config, get_turku_resource, set_syncher_object_field, set_syncher_tku_translated_field, @@ -24,17 +19,6 @@ UTC_TIMEZONE = pytz.timezone("UTC") SERVICE_AS_SERVICE_NODE_PREFIX = "service_" -""" -List of All external importers that imports data to the services list. -If importer is not added to the list, its imported data will be deleted during importing of Units and Services. -""" -EXTERNAL_IMPORTERS = [ - GasFillingStationImporter, - ChargingStationImporter, - BikeServiceStationImporter, - BicycleStandImporter, -] - BLACKLISTED_SERVICE_NODES = [ "2_1", "2_2", @@ -65,18 +49,20 @@ def _import_service_nodes(self, keyword_handler): self._handle_service_node(parent_node, keyword_handler) if not self.delete_external_sources: - for importer in EXTERNAL_IMPORTERS: - self._handle_external_service_node(importer) + for config in get_external_sources_yaml_config(): + self._handle_external_service_node(config) self.nodesyncher.finish() - def _handle_external_service_node(self, importer): + def _handle_external_service_node(self, config): """ Mark service_node that has been imported from external source. If not marked the nodesyncher.finish() will delete the service node. """ try: - service_node = ServiceNode.objects.get(name=importer.SERVICE_NODE_NAME) + service_node = ServiceNode.objects.get( + name=config["service_node"]["name"]["fi"] + ) service_node = self.nodesyncher.get(service_node.id) self.nodesyncher.mark(service_node) except ServiceNode.DoesNotExist: @@ -89,18 +75,18 @@ def _import_services(self, keyword_handler): self._handle_service(service, keyword_handler) if not self.delete_external_sources: - for importer in EXTERNAL_IMPORTERS: - self._handle_external_service(importer) + for config in get_external_sources_yaml_config(): + self._handle_external_service(config) self.servicesyncher.finish() - def _handle_external_service(self, importer): + def _handle_external_service(self, config): """ Mark service that has been imported from external source. If not marked the servicesyncher.finish() will delete the service. """ try: - service = Service.objects.get(name=importer.SERVICE_NAME) + service = Service.objects.get(name=config["service"]["name"]["fi"]) service = self.servicesyncher.get(service.id) self.servicesyncher.mark(service) except Service.DoesNotExist: From a2e5ec28f282dc3f3ae2691fae2c7a0ebf4256f9 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:54:42 +0200 Subject: [PATCH 068/188] Remove useless constants --- smbackend_turku/importers/constants.py | 52 -------------------------- 1 file changed, 52 deletions(-) diff --git a/smbackend_turku/importers/constants.py b/smbackend_turku/importers/constants.py index fb71ba9ad..e69de29bb 100644 --- a/smbackend_turku/importers/constants.py +++ b/smbackend_turku/importers/constants.py @@ -1,52 +0,0 @@ -GAS_FILLING_STATION_SERVICE_NODE_NAME = "Kaasutankkausasemat" -GAS_FILLING_STATION_SERVICE_NAME = "Kaasutankkausasema" -GAS_FILLING_STATION_SERVICE_NODE_NAMES = { - "fi": GAS_FILLING_STATION_SERVICE_NODE_NAME, - "sv": "Tankstationer med gas", - "en": "Gas filling stations", -} - -GAS_FILLING_STATION_SERVICE_NAMES = { - "fi": GAS_FILLING_STATION_SERVICE_NAME, - "sv": "Tankstation med gas", - "en": "Gas filling station", -} - -CHARGING_STATION_SERVICE_NODE_NAME = "Autojen sähkölatauspisteet" -CHARGING_STATION_SERVICE_NAME = "Autojen sähkölatauspiste" -CHARGING_STATION_SERVICE_NODE_NAMES = { - "fi": CHARGING_STATION_SERVICE_NODE_NAME, - "sv": "Elladdningsstationer för bilar", - "en": "Car e-charging points", -} -CHARGING_STATION_SERVICE_NAMES = { - "fi": CHARGING_STATION_SERVICE_NAME, - "sv": "Elladdningsstation för bilar", - "en": "Car e-charging point", -} - -BICYCLE_STAND_SERVICE_NODE_NAME = "Pyöräpysäköinnit" -BICYCLE_STAND_SERVICE_NAME = "Pyöräpysäköinti" -BICYCLE_STAND_SERVICE_NODE_NAMES = { - "fi": BICYCLE_STAND_SERVICE_NODE_NAME, - "sv": "Cykelparkeringar", - "en": "Bicycle parkings", -} -BICYCLE_STAND_SERVICE_NAMES = { - "fi": BICYCLE_STAND_SERVICE_NAME, - "sv": "Cykelparkering", - "en": "Bicycle parking", -} - -BIKE_SERVICE_STATION_SERVICE_NODE_NAME = "Pyöränkorjauspisteet" -BIKE_SERVICE_STATION_SERVICE_NAME = "Pyöränkorjauspiste" -BIKE_SERVICE_STATION_SERVICE_NODE_NAMES = { - "fi": BIKE_SERVICE_STATION_SERVICE_NODE_NAME, - "sv": "Cykelservicestationer", - "en": "Bike service stations", -} -BIKE_SERVICE_STATION_SERVICE_NAMES = { - "fi": BIKE_SERVICE_STATION_SERVICE_NAME, - "sv": "Cykelservicestation", - "en": "Bike service station", -} From 5b9ce8741d2bfff16ee7a6c14ba1952ce64d74b2 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 14:56:25 +0200 Subject: [PATCH 069/188] Use BaseExternalSource class for importing --- smbackend_turku/importers/bicycle_stands.py | 124 +------------------- 1 file changed, 6 insertions(+), 118 deletions(-) diff --git a/smbackend_turku/importers/bicycle_stands.py b/smbackend_turku/importers/bicycle_stands.py index b8066e360..0136d6704 100644 --- a/smbackend_turku/importers/bicycle_stands.py +++ b/smbackend_turku/importers/bicycle_stands.py @@ -1,140 +1,28 @@ -from datetime import datetime - -from django.conf import settings - from mobility_data.importers.bicycle_stands import ( create_bicycle_stand_content_type, - delete_bicycle_stands as mobility_data_delete_bicycle_stands, get_bicycle_stand_objects, ) -from mobility_data.importers.utils import create_mobile_unit_as_unit_reference -from services.management.commands.services_import.services import ( - update_service_counts, - update_service_node_counts, -) -from services.models import Service, ServiceNode, Unit, UnitServiceDetails -from smbackend_turku.importers.constants import ( - BICYCLE_STAND_SERVICE_NAME, - BICYCLE_STAND_SERVICE_NAMES, - BICYCLE_STAND_SERVICE_NODE_NAME, - BICYCLE_STAND_SERVICE_NODE_NAMES, -) -from smbackend_turku.importers.utils import ( - create_service, - create_service_node, - delete_external_source, - get_municipality, - set_field, - set_service_names_field, - set_syncher_object_field, - set_tku_translated_field, - UTC_TIMEZONE, -) +from smbackend_turku.importers.utils import BaseExternalSource -class BicycleStandImporter: - - SERVICE_ID = settings.BICYCLE_STANDS_IDS["service"] - SERVICE_NODE_ID = settings.BICYCLE_STANDS_IDS["service_node"] - UNITS_ID_OFFSET = settings.BICYCLE_STANDS_IDS["units_offset"] - SERVICE_NODE_NAME = BICYCLE_STAND_SERVICE_NODE_NAME - SERVICE_NAME = BICYCLE_STAND_SERVICE_NAME - SERVICE_NODE_NAMES = BICYCLE_STAND_SERVICE_NODE_NAMES - SERVICE_NAMES = BICYCLE_STAND_SERVICE_NAMES - - def __init__(self, logger=None, root_service_node_name=None, test_data=None): +class BicycleStandImporter(BaseExternalSource): + def __init__(self, logger=None, config=None, test_data=None): + super().__init__(config) self.logger = logger - self.root_service_node_name = root_service_node_name self.test_data = test_data def import_bicycle_stands(self): - service_id = self.SERVICE_ID self.logger.info("Importing Bicycle Stands...") content_type = create_bicycle_stand_content_type() - saved_bicycle_stands = 0 filtered_objects = get_bicycle_stand_objects() - for i, data_obj in enumerate(filtered_objects): - unit_id = i + self.UNITS_ID_OFFSET - obj = Unit(id=unit_id) - set_field(obj, "location", data_obj.geometry) - set_tku_translated_field(obj, "name", data_obj.prefix_name) - if data_obj.street_address: - set_tku_translated_field(obj, "street_address", data_obj.street_address) - else: - set_tku_translated_field(obj, "street_address", data_obj.name) - extra = {} - extra["model"] = data_obj.model - extra["maintained_by_turku"] = data_obj.maintained_by_turku - extra["number_of_stands"] = data_obj.number_of_stands - extra["number_of_places"] = data_obj.number_of_places - extra["hull_lockable"] = data_obj.hull_lockable - extra["covered"] = data_obj.covered - # Add non prefixed names to extra, so that the front end does not need - # to remove the prefix. - extra["name_fi"] = data_obj.name["fi"] - extra["name_sv"] = data_obj.name["sv"] - extra["name_en"] = data_obj.name["en"] - set_field(obj, "extra", extra) - if data_obj.maintained_by_turku: - # 1 = self produced - set_syncher_object_field(obj, "provider_type", 1) - else: - # 7 = Unknown production method - set_syncher_object_field(obj, "provider_type", 7) - - try: - service = Service.objects.get(id=service_id) - except Service.DoesNotExist: - self.logger.warning('Service "{}" does not exist!'.format(service_id)) - continue - UnitServiceDetails.objects.get_or_create(unit=obj, service=service) - service_nodes = ServiceNode.objects.filter(related_services=service) - obj.service_nodes.add(*service_nodes) - set_field(obj, "root_service_nodes", obj.get_root_service_nodes()[0]) - municipality = get_municipality(data_obj.city) - set_field(obj, "municipality", municipality) - obj.last_modified_time = datetime.now(UTC_TIMEZONE) - set_service_names_field(obj) - if data_obj.related_unit: - data_obj.related_unit.related_units.add(obj) - data_obj.related_unit.save() - obj.save() - create_mobile_unit_as_unit_reference(unit_id, content_type) - saved_bicycle_stands += 1 - update_service_node_counts() - update_service_counts() + super().save_objects_as_units(filtered_objects, content_type) def delete_bicycle_stands(**kwargs): importer = BicycleStandImporter(**kwargs) - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_bicycle_stands, - ) - update_service_node_counts() - update_service_counts() + importer.delete_external_source() def import_bicycle_stands(**kwargs): importer = BicycleStandImporter(**kwargs) - # Delete all Bicycle stand units before storing, to ensure stored data is up to date. - delete_external_source( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - mobility_data_delete_bicycle_stands, - ) - - create_service_node( - importer.SERVICE_NODE_ID, - importer.SERVICE_NODE_NAME, - importer.root_service_node_name, - importer.SERVICE_NODE_NAMES, - ) - create_service( - importer.SERVICE_ID, - importer.SERVICE_NODE_ID, - importer.SERVICE_NAME, - importer.SERVICE_NAMES, - ) importer.import_bicycle_stands() From 5ec3b520738abd696c2fa8c8e46ac691f097eff0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 15:04:55 +0200 Subject: [PATCH 070/188] Remove external sources id settings --- config_dev.env.example | 4 ---- 1 file changed, 4 deletions(-) diff --git a/config_dev.env.example b/config_dev.env.example index 4127bfee9..01f85a62a 100644 --- a/config_dev.env.example +++ b/config_dev.env.example @@ -161,10 +161,6 @@ TRAFFIC_COUNTER_OBSERVATIONS_BASE_URL=https://data.turku.fi/2yxpk2imqi2mzxpa6e6k LAM_COUNTER_STATIONS_URL=https://tie.digitraffic.fi/api/v3/metadata/tms-stations LAM_COUNTER_API_BASE_URL=https://tie-lam-test.digitraffic.fi -GAS_FILLING_STATIONS_IDS=service_node=200000,service=200000,units_offset=200000 -CHARGING_STATIONS_IDS=service_node=300000,service=300000,units_offset=300000 -BICYCLE_STANDS_IDS=service_node=400000,service=400000,units_offset=400000 -BIKE_SERVICE_STATIONS_IDS=service_node=500000,service=500000,units_offset=500000 MOBILITY_DATA_CHARGING_STATIONS_URL=https://services1.arcgis.com/rhs5fjYxdOG1Et61/ArcGIS/rest/services/GasFillingStations/FeatureServer/0/query?f=json&where=1%3D1&outFields=OPERATOR%2CLAT%2CLON%2CSTATION_NAME%2CADDRESS%2CCITY%2CZIP_CODE%2CLNG_CNG%2CObjectId MOBILITY_DATA_GAS_FILLING_STATIONS_URL=https://services1.arcgis.com/rhs5fjYxdOG1Et61/ArcGIS/rest/services/ChargingStations/FeatureServer/0/query?f=json&where=1%20%3D%201%20OR%201%20%3D%201&returnGeometry=true&spatialRel=esriSpatialRelIntersects&outFields=LOCATION_ID%2CNAME%2CADDRESS%2CURL%2COBJECTID%2CTYPE MOBILITY_DATA_GEOMETRY_URL=https://tie.digitraffic.fi/api/v3/data/traffic-messages/area-geometries?id=11&lastUpdated=false From 17f71042e42eb4d09984977524be7d3d8ce93f29 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 15:07:36 +0200 Subject: [PATCH 071/188] Refactor dynamic function generation --- .../commands/turku_services_import.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/smbackend_turku/management/commands/turku_services_import.py b/smbackend_turku/management/commands/turku_services_import.py index a9d324523..5c3ff0216 100644 --- a/smbackend_turku/management/commands/turku_services_import.py +++ b/smbackend_turku/management/commands/turku_services_import.py @@ -33,7 +33,18 @@ from smbackend_turku.importers.utils import get_external_source_config # noqa: F401 from smbackend_turku.importers.utils import get_configured_external_sources_names - +IMPORTER_FUNCTIONS_CODE = """ +@db.transaction.atomic +def import_{name}(self): + config = get_external_source_config("{name}") + import_{name}(logger=self.logger, config=config) + print('IMPORT') +@db.transaction.atomic +def delete_{name}(self): + config = get_external_source_config("{name}") + delete_{name}(logger=self.logger, config=config) + print('DELETE') +""" class Command(BaseCommand): help = "Import services from City of Turku APIs and from external sources." @@ -55,18 +66,7 @@ class Command(BaseCommand): supported_languages = [lang[0] for lang in settings.LANGUAGES] for name in external_sources: - code = f""" -@db.transaction.atomic -def import_{name}(self): - config = get_external_source_config("{name}") - import_{name}(logger=self.logger, config=config) - print('IMPORT') -@db.transaction.atomic -def delete_{name}(self): - config = get_external_source_config("{name}") - delete_{name}(logger=self.logger, config=config) - print('DELETE') - """ + code = IMPORTER_FUNCTIONS_CODE.format(name=name) exec(code) def __init__(self): From 49ec0b06545be582ad034d3ad8796a2e0890af78 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 31 Jan 2023 15:08:14 +0200 Subject: [PATCH 072/188] Populate extre field in class --- mobility_data/importers/bicycle_stands.py | 86 +++++++++++------------ 1 file changed, 41 insertions(+), 45 deletions(-) diff --git a/mobility_data/importers/bicycle_stands.py b/mobility_data/importers/bicycle_stands.py index 54afdca98..78cb05db2 100644 --- a/mobility_data/importers/bicycle_stands.py +++ b/mobility_data/importers/bicycle_stands.py @@ -54,26 +54,27 @@ class BicyleStand: WFS_HULL_LOCKABLE_STR = "runkolukitusmahdollisuus" GEOJSON_HULL_LOCKABLE_STR = "runkolukittava" COVERED_IN_STR = "katettu" + EXTRA_FIELDS = [ + "maintained_by_turku", + "covered", + "hull_lockable", + "model", + "number_of_places", + "number_of_stands", + ] def __init__(self): self.geometry = None - self.model = None - self.number_of_stands = None - self.number_of_places = None # The total number of places for bicycles. - self.hull_lockable = None - self.covered = None self.city = None - self.street_address = None - self.maintained_by_turku = None self.name = {} self.prefix_name = {} - self.street_address = {} + self.address = {} self.related_unit = None + self.extra = {f: None for f in self.EXTRA_FIELDS} def set_geojson_feature(self, feature): name = feature["kohde"].as_string().strip() unit_name = name.split(",")[0] - self.geometry = GEOSGeometry(feature.geom.wkt, srid=GEOJSON_SOURCE_DATA_SRID) self.geometry.transform(settings.DEFAULT_SRID) units_qs = Unit.objects.filter(name=unit_name) @@ -84,24 +85,26 @@ def set_geojson_feature(self, feature): self.related_unit = unit break - self.number_of_stands = feature["telineitä"].as_int() - self.number_of_places = feature["paikkoja"].as_int() + self.extra["number_of_stands"] = feature["telineitä"].as_int() + self.extra["number_of_places"] = feature["paikkoja"].as_int() model_elem = feature["pys.malli"].as_string() if model_elem: - self.model = model_elem + self.extra["model"] = model_elem + else: + self.extra["model"] = None quality_elem = feature["laatutaso"].as_string() if quality_elem: quality_text = quality_elem.lower() if self.GEOJSON_HULL_LOCKABLE_STR in quality_text: - self.hull_lockable = True + self.extra["hull_lockable"] = True else: - self.hull_lockable = False + self.extra["hull_lockable"] = False if self.COVERED_IN_STR in quality_text: - self.covered = True + self.extra["covered"] = True else: - self.covered = False + self.extra["covered"] = False self.city = get_municipality_name(self.geometry) self.name["fi"] = name @@ -128,56 +131,55 @@ def set_geojson_feature(self, feature): # The last part is always the number address_number = address[-1] translated_street_names = get_street_name_translations(street_name, self.city) - self.street_address["fi"] = f"{translated_street_names['fi']} {address_number}" - self.street_address["sv"] = f"{translated_street_names['sv']} {address_number}" - self.street_address["en"] = f"{translated_street_names['en']} {address_number}" + self.address["fi"] = f"{translated_street_names['fi']} {address_number}" + self.address["sv"] = f"{translated_street_names['sv']} {address_number}" + self.address["en"] = f"{translated_street_names['en']} {address_number}" def set_gml_feature(self, feature): object_id = feature["id"].as_string() # If ObjectId is set to "0", the bicycle stand is not maintained by Turku if object_id == "0": - self.maintained_by_turku = False + self.extra["maintained_by_turku"] = False else: - self.maintained_by_turku = True + self.extra["maintained_by_turku"] = True self.geometry = GEOSGeometry(feature.geom.wkt, srid=WFS_SOURCE_DATA_SRID) self.geometry.transform(settings.DEFAULT_SRID) model_elem = feature["Malli"] if model_elem is not None: - self.model = model_elem.as_string() + self.extra["model"] = model_elem.as_string() + else: + self.extra["model"] = None num_stands_elem = feature["Lukumaara"] if num_stands_elem is not None: num = num_stands_elem.as_int() # for bicycle stands that are Not maintained by Turku # the number of stands is set to 0 in the input data # but in reality there is no data so None is set. - if num == 0 and not self.maintained_by_turku: - self.number_of_stands = None + if num == 0 and not self.extra["maintained_by_turku"]: + self.extra["number_of_stands"] = None else: - self.number_of_stands = num + self.extra["number_of_stands"] = num num_places_elem = feature["Pyorapaikkojen_lukumaara"].as_string() - if num_places_elem: # Parse the numbers inside the string and finally sum them. # The input can contain string such as "8 runkolukittavaa ja 10 ei runkolukittavaa paikkaa" numbers = [int(s) for s in num_places_elem.split() if s.isdigit()] - self.number_of_places = sum(numbers) + self.extra["number_of_places"] = sum(numbers) quality_elem = feature["Pyorapaikkojen_laatutaso"].as_string() - if quality_elem: quality_text = quality_elem.lower() if self.WFS_HULL_LOCKABLE_STR in quality_text: - self.hull_lockable = True + self.extra["hull_lockable"] = True else: - self.hull_lockable = False - + self.extra["hull_lockable"] = False if self.COVERED_IN_STR in quality_text: - self.covered = True + self.extra["covered"] = True else: - self.covered = False + self.extra["covered"] = False self.city = get_municipality_name(self.geometry) full_names = get_closest_address_full_name(self.geometry) self.name[FI_KEY] = full_names[FI_KEY] @@ -226,11 +228,11 @@ def get_bicycle_stand_objects(data_source=None): bicycle_stand.set_geojson_feature(feature) if ( bicycle_stand.name[FI_KEY] not in external_stands - and not bicycle_stand.maintained_by_turku + and not bicycle_stand.extra["maintained_by_turku"] ): external_stands[bicycle_stand.name[FI_KEY]] = True bicycle_stands.append(bicycle_stand) - elif bicycle_stand.maintained_by_turku: + elif bicycle_stand.extra["maintained_by_turku"]: bicycle_stands.append(bicycle_stand) logger.info(f"Retrieved {len(bicycle_stands)} bicycle stands.") @@ -258,17 +260,11 @@ def save_to_database(objects, delete_tables=True): for object in objects: mobile_unit = MobileUnit.objects.create( content_type=content_type, + extra=object.extra, ) - extra = {} - extra["model"] = object.model - extra["maintained_by_turku"] = object.maintained_by_turku - extra["number_of_stands"] = object.number_of_stands - extra["number_of_places"] = object.number_of_places - extra["hull_lockable"] = object.hull_lockable - extra["covered"] = object.covered - mobile_unit.extra = extra + mobile_unit.geometry = object.geometry set_translated_field(mobile_unit, "name", object.name) - if object.street_address: - set_translated_field(mobile_unit, "address", object.street_address) + if object.address: + set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() From e43de5e05c20e89668d41eb2c1ad41f5b6dd49ff Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 08:46:01 +0200 Subject: [PATCH 073/188] Serialize the correct id and add unit_id to field --- mobility_data/api/serializers/mobile_unit.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/mobility_data/api/serializers/mobile_unit.py b/mobility_data/api/serializers/mobile_unit.py index 8b0856440..a88b09c85 100644 --- a/mobility_data/api/serializers/mobile_unit.py +++ b/mobility_data/api/serializers/mobile_unit.py @@ -71,6 +71,7 @@ class Meta: "geometry", "geometry_coords", "extra", + "unit_id", ] # Contains the corresponding field names of the MobileUnit model if they differs @@ -102,7 +103,9 @@ def to_representation(self, obj): representation[field] = unit.municipality.id else: representation[field] = getattr(unit, key) - + # Serialize the MobileUnit id, otherwise would serialize the serivce_unit id. + if field == "id": + representation["id"] = obj.id # The location field must be serialized with its wkt value. if unit.location: representation["geometry"] = unit.location.wkt From 466df8e4458633c402f1b6a64281219a166abd2f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 08:50:26 +0200 Subject: [PATCH 074/188] Add municipality --- mobility_data/importers/bicycle_stands.py | 27 ++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/mobility_data/importers/bicycle_stands.py b/mobility_data/importers/bicycle_stands.py index 78cb05db2..99f1888dc 100644 --- a/mobility_data/importers/bicycle_stands.py +++ b/mobility_data/importers/bicycle_stands.py @@ -9,7 +9,11 @@ from django.conf import settings from django.contrib.gis.gdal import DataSource from django.contrib.gis.geos import GEOSGeometry -from munigeo.models import AdministrativeDivision, AdministrativeDivisionGeometry +from munigeo.models import ( + AdministrativeDivision, + AdministrativeDivisionGeometry, + Municipality, +) from mobility_data.models import MobileUnit from services.models import Unit @@ -65,7 +69,7 @@ class BicyleStand: def __init__(self): self.geometry = None - self.city = None + self.municipality = None self.name = {} self.prefix_name = {} self.address = {} @@ -106,7 +110,12 @@ def set_geojson_feature(self, feature): else: self.extra["covered"] = False - self.city = get_municipality_name(self.geometry) + municipality_name = get_municipality_name(self.geometry) + try: + self.municipality = Municipality.objects.get(name=municipality_name) + except Municipality.DoesNotExist: + self.municipality = None + self.name["fi"] = name # If related unit is known, use its translated names if self.related_unit: @@ -130,7 +139,9 @@ def set_geojson_feature(self, feature): else: # The last part is always the number address_number = address[-1] - translated_street_names = get_street_name_translations(street_name, self.city) + translated_street_names = get_street_name_translations( + street_name, municipality_name + ) self.address["fi"] = f"{translated_street_names['fi']} {address_number}" self.address["sv"] = f"{translated_street_names['sv']} {address_number}" self.address["en"] = f"{translated_street_names['en']} {address_number}" @@ -180,7 +191,12 @@ def set_gml_feature(self, feature): self.extra["covered"] = True else: self.extra["covered"] = False - self.city = get_municipality_name(self.geometry) + try: + self.municipality = Municipality.objects.get( + name=get_municipality_name(self.geometry) + ) + except Municipality.DoesNotExist: + self.municipality = None full_names = get_closest_address_full_name(self.geometry) self.name[FI_KEY] = full_names[FI_KEY] self.name[SV_KEY] = full_names[SV_KEY] @@ -261,6 +277,7 @@ def save_to_database(objects, delete_tables=True): mobile_unit = MobileUnit.objects.create( content_type=content_type, extra=object.extra, + municipality=object.municipality, ) mobile_unit.geometry = object.geometry From 9508c215774e28c00cd21f9136224e45fa8a694f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 08:51:01 +0200 Subject: [PATCH 075/188] Test municipality --- mobility_data/tests/test_import_bicycle_stands.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/mobility_data/tests/test_import_bicycle_stands.py b/mobility_data/tests/test_import_bicycle_stands.py index 9d85ba92a..e5b9a7ae8 100644 --- a/mobility_data/tests/test_import_bicycle_stands.py +++ b/mobility_data/tests/test_import_bicycle_stands.py @@ -34,6 +34,7 @@ def test_geojson_import( assert kupittaan_palloiluhalli.extra["covered"] is True assert turun_amk.extra["hull_lockable"] is True assert turun_amk.extra["covered"] is False + assert turun_amk.municipality.name == "Turku" @pytest.mark.django_db @@ -55,6 +56,7 @@ def test_wfs_importer( stand_external = MobileUnit.objects.all()[2] assert stand_normal.name_fi == "Linnanpuisto" assert stand_normal.name_sv == "Slottsparken" + assert stand_normal.municipality.name == "Turku" extra = stand_normal.extra assert extra["model"] == "Normaali" assert extra["maintained_by_turku"] is True @@ -62,7 +64,6 @@ def test_wfs_importer( assert extra["hull_lockable"] is False assert extra["number_of_places"] == 24 assert extra["number_of_stands"] == 2 - assert extra["number_of_stands"] == 2 assert stand_covered_hull_lockable.name == "Pitkäpellonkatu 7" assert stand_covered_hull_lockable.name_sv == "Långåkersgatan 7" extra = stand_covered_hull_lockable.extra From 7eec764128b18ea06de7c7e35fd3a586492dd4ee Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 08:51:49 +0200 Subject: [PATCH 076/188] Add service names --- mobility_data/importers/charging_stations.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mobility_data/importers/charging_stations.py b/mobility_data/importers/charging_stations.py index e395c445f..1d0badaa5 100644 --- a/mobility_data/importers/charging_stations.py +++ b/mobility_data/importers/charging_stations.py @@ -6,7 +6,6 @@ from django.contrib.gis.geos import Point from mobility_data.models import MobileUnit -from smbackend_turku.importers.constants import CHARGING_STATION_SERVICE_NAMES from .utils import ( delete_mobile_units, @@ -22,6 +21,11 @@ logger = logging.getLogger("mobility_data") +CHARGING_STATION_SERVICE_NAMES = { + "fi": "Autojen sähkölatauspiste", + "sv": "Elladdningsstation för bilar", + "en": "Car e-charging point", +} SOURCE_DATA_FILE_NAME = "LatauspisteetTurku.csv" SOURCE_DATA_SRID = 3877 CONTENT_TYPE_NAME = "ChargingStation" From 6215fdde537611639cb25a6363fd34761e1ba38f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 08:53:11 +0200 Subject: [PATCH 077/188] Import/use service names from importer --- mobility_data/tests/test_import_charging_stations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobility_data/tests/test_import_charging_stations.py b/mobility_data/tests/test_import_charging_stations.py index 33826361e..e5ce52ac3 100644 --- a/mobility_data/tests/test_import_charging_stations.py +++ b/mobility_data/tests/test_import_charging_stations.py @@ -1,9 +1,11 @@ import pytest from munigeo.models import Address -from mobility_data.importers.charging_stations import CONTENT_TYPE_NAME +from mobility_data.importers.charging_stations import ( + CHARGING_STATION_SERVICE_NAMES, + CONTENT_TYPE_NAME, +) from mobility_data.models import ContentType, MobileUnit -from smbackend_turku.importers.constants import CHARGING_STATION_SERVICE_NAMES from .utils import import_command From e85f6bca854f6a22ae97907433b22313217907dd Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:30:01 +0200 Subject: [PATCH 078/188] Update info about external sources --- smbackend_turku/README.md | 57 ++++++++++----------------------------- 1 file changed, 14 insertions(+), 43 deletions(-) diff --git a/smbackend_turku/README.md b/smbackend_turku/README.md index c54e28e8c..c8b65ec00 100644 --- a/smbackend_turku/README.md +++ b/smbackend_turku/README.md @@ -50,59 +50,30 @@ Note, if geo-search addresses are imported this might take ~45minutes. ## Importing external data sources -Importing from external data sources should always be done after importing the services and units. -To import the mobility data, currently imports: gas filling stations, bicycle stands, charging stations and bike service stations. +Importing from external data sources should always be done after importing the services and units. External data sources are configured in: +smbackend_turku/importers/data/external_sources_config.yml + ``` -./manage.py turku_services_import mobility_data +./manage.py turku_services_import external_sorce ``` To delete all data imported from external sources: ``` ./manage.py turku_services_import services units --delete-external-sources ``` -To delete a specific imported external data source: -e.g. remove bicycle_stands -``` -./manage.py turku_services_import bicycle_stands --delete-external-source -``` -Currently following importers import to the mobility view by setting -a id, which is used to retrieve the data from the service_unit table: -gas_filling_stations and bicycle_stands. e.g. These -importers import data to both the services list and mobility view. - -When importing services and units the ids are received from the source. Therefore the ids for the external sources must be manually set to avoid id collisions. -The ids are set in a dict in the .env file with following keys: -* service_node is the id of the service_node, recommended value < 3000000 -* service is the id of the service, recommended value > 1000 -* units_offset is the offset that will be given to the imported units ids, recommended value > 10000. -Example: -GAS_FILLING_STATIONS_IDS=service_node=200000,service=200000,units_offset=200000 - -### Gas filling stations -Add following line to the .env file: -GAS_FILLING_STATIONS_IDS=service_node=200000,service=200000,units_offset=200000 - -To import type: +To import a specific external data source give the name of the external data source +defined in the config file as argument. +e.g.: ``` ./manage.py turku_services_import gas_filling_stations ``` +Imports external data source named gas_filling_stations. -### Bicycle stands -Add following line to the .env file: -BICYCLE_STANDS_IDS=service_node=400000,service=400000,units_offset=400000 - -To import type: -``` -./manage.py turku_services_import bicycle_stands -``` - -### Bike service stations -Add following line to the .env file: -BIKE_SERVICE_STATIONS_IDS=service_node=500000,service=500000,units_offset=500000 -To import type: +To delete a specific imported external data source: +e.g., remove external source named bicycle_stands. ``` -./manage.py turku_services_import bike_service_stations +./manage.py turku_services_import --delete-external-source bicycle_stands ``` -### Mobility data -For detailed information about importing, see: /mobility_data/README.md - +### Note +When importing services and units the ids are received from the source. Therefore the ids for the external sources must be manually set to avoid id collisions. +The ids are configured in the smbackend_turku/importers/data/external_sources_config.yml From c49206f7a3f3b912eefc98165f54098726b31ef0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:30:25 +0200 Subject: [PATCH 079/188] Rename mobility_data task to external_sources --- smbackend_turku/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/smbackend_turku/tasks.py b/smbackend_turku/tasks.py index 517983c7f..7fef1c361 100644 --- a/smbackend_turku/tasks.py +++ b/smbackend_turku/tasks.py @@ -82,5 +82,5 @@ def import_charging_stations(name="import_charging_stations"): @shared_task -def import_mobility_data(name="import_mobility_data"): - management.call_command("turku_services_import", "mobility_data") +def import_external_sources(name="import_external_sources"): + management.call_command("turku_services_import", "external_sources") From 4a74a748e68779cb624eb2bfa83b0cd8564cf6ed Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:30:53 +0200 Subject: [PATCH 080/188] Delete --- .../importers/data/external_units_config.yml | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 smbackend_turku/importers/data/external_units_config.yml diff --git a/smbackend_turku/importers/data/external_units_config.yml b/smbackend_turku/importers/data/external_units_config.yml deleted file mode 100644 index 78c48a58f..000000000 --- a/smbackend_turku/importers/data/external_units_config.yml +++ /dev/null @@ -1,85 +0,0 @@ -external_data_sources: - - name: bike_service_stations - # Name of the class - importer_name: BikeServiceStationImporter - root_service_node_name: Vapaa-aika - units_offset: 500000 - # 1 = self produced - provider_type: 1 - service: - id: 500000 - name: - fi: Pyöränkorjauspiste - sv: Cykelservicestation - en: Bike service station - service_node: - id: 500000 - name: - fi: Pyöränkorjauspisteet - sv: Cykelservicestationer - en: Bike service stations - # When importing, create mobile_units that has a reference id to the unit. - # The data of the mobile_unit is then serialized from the services_unit table. - create_mobile_units_with_unit_reference: True - # For deleting - mobility_data_content_type_name: BikeServiceStation - - - name: gas_filling_stations - importer_name: GasFillingStationImporter - root_service_node_name: Vapaa-aika - units_offset: 200000 - service: - id: 200000 - name: - fi: Kaasutankkausasema - sv: Tankstation med gas - en: Gas filling station - service_node: - id: 200000 - name: - fi: Kaasutankkausasemat - sv: Tankstationer med gas - en: Gas filling stations - create_mobile_units_with_unit_reference: True - # For deleting - mobility_data_content_type_name: GasFillingStation - - - name: charging_stations - importer_name: ChargingStationImporter - root_service_node_name: Vapaa-aika - units_offset: 300000 - service: - id: 300000 - name: - fi: Autojen sähkölatauspiste - sv: Elladdningsstation för bilar - en: Car e-charging point - service_node: - id: 300000 - name: - fi: Autojen sähkölatauspisteet - sv: Elladdningsstationer för bilar - en: Car e-charging points - create_mobile_units_with_unit_reference: True - # For deleting - mobility_data_content_type_name: ChargingStation - - - name: bicycle_stands - importer_name: BicycleStandsImporter - root_service_node_name: Vapaa-aika - units_offset: 400000 - service: - id: 400000 - name: - fi: Pyöräpysäköinti - sv: Cykelparkering - en: Bicycle parking - service_node: - id: 400000 - name: - fi: Pyöräpysäköinnit - sv: Cykelparkeringar - en: Bicycle parkings - create_mobile_units_with_unit_reference: True - # For deleting - mobility_data_content_type_name: BicycleStand \ No newline at end of file From 905b39bcb23823f59a4cc3ae988692e5e50f0ed0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:31:11 +0200 Subject: [PATCH 081/188] Add comments --- .../data/external_sources_config.yml | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 smbackend_turku/importers/data/external_sources_config.yml diff --git a/smbackend_turku/importers/data/external_sources_config.yml b/smbackend_turku/importers/data/external_sources_config.yml new file mode 100644 index 000000000..b5643ffb2 --- /dev/null +++ b/smbackend_turku/importers/data/external_sources_config.yml @@ -0,0 +1,84 @@ +external_data_sources: + # The name must be equal(excluding the 'import_' prefix) to the name of function + # that creates the importer instance add calls its importer function + - name: bike_service_stations + root_service_node_name: Vapaa-aika + # The offset of the ids of the units to be created. As the ids of the units comes + # from the source data, an offset must be given to avoid id collision. + units_offset: 500000 + # 1 = self produced, see units model for more details of provider types. + provider_type: 1 + # ID and name of the service that will be created for the units + service: + id: 500000 + name: + fi: Pyöränkorjauspiste + sv: Cykelservicestation + en: Bike service station + # ID and name of the service node that will be created. + service_node: + id: 500000 + name: + fi: Pyöränkorjauspisteet + sv: Cykelservicestationer + en: Bike service stations + # When importing, create mobile_units that has a reference id to the unit. + # The data of the mobile_unit is then serialized from the services_unit table. + create_mobile_units_with_unit_reference: True + # If mobile units area created, the name of the content type that is + # created during import must be given. + mobility_data_content_type_name: BikeServiceStation + + - name: gas_filling_stations + root_service_node_name: Vapaa-aika + units_offset: 200000 + service: + id: 200000 + name: + fi: Kaasutankkausasema + sv: Tankstation med gas + en: Gas filling station + service_node: + id: 200000 + name: + fi: Kaasutankkausasemat + sv: Tankstationer med gas + en: Gas filling stations + create_mobile_units_with_unit_reference: True + mobility_data_content_type_name: GasFillingStation + + - name: charging_stations + root_service_node_name: Vapaa-aika + units_offset: 300000 + service: + id: 300000 + name: + fi: Autojen sähkölatauspiste + sv: Elladdningsstation för bilar + en: Car e-charging point + service_node: + id: 300000 + name: + fi: Autojen sähkölatauspisteet + sv: Elladdningsstationer för bilar + en: Car e-charging points + create_mobile_units_with_unit_reference: True + mobility_data_content_type_name: ChargingStation + + - name: bicycle_stands + root_service_node_name: Vapaa-aika + units_offset: 400000 + service: + id: 400000 + name: + fi: Pyöräpysäköinti + sv: Cykelparkering + en: Bicycle parking + service_node: + id: 400000 + name: + fi: Pyöräpysäköinnit + sv: Cykelparkeringar + en: Bicycle parkings + create_mobile_units_with_unit_reference: True + mobility_data_content_type_name: BicycleStand \ No newline at end of file From 774e1571f8b7fa8584b22fd1ebe73918b2a30293 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:32:20 +0200 Subject: [PATCH 082/188] Change name of the config file --- smbackend_turku/importers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smbackend_turku/importers/utils.py b/smbackend_turku/importers/utils.py index 2d0290677..ea399347d 100644 --- a/smbackend_turku/importers/utils.py +++ b/smbackend_turku/importers/utils.py @@ -33,7 +33,7 @@ UTC_TIMEZONE = pytz.timezone("UTC") data_path = os.path.join(os.path.dirname(__file__), "data") -EXTERNAL_SOURCES_CONFIG_FILE = f"{data_path}/external_units_config.yml" +EXTERNAL_SOURCES_CONFIG_FILE = f"{data_path}/external_sources_config.yml" def get_external_sources_yaml_config(): From 76ef96b1771b5f4db32fa9b3d720f8a456bba417 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:36:34 +0200 Subject: [PATCH 083/188] Add external_source importer target --- .../commands/turku_services_import.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/smbackend_turku/management/commands/turku_services_import.py b/smbackend_turku/management/commands/turku_services_import.py index 5c3ff0216..d3bff40eb 100644 --- a/smbackend_turku/management/commands/turku_services_import.py +++ b/smbackend_turku/management/commands/turku_services_import.py @@ -38,18 +38,18 @@ def import_{name}(self): config = get_external_source_config("{name}") import_{name}(logger=self.logger, config=config) - print('IMPORT') @db.transaction.atomic def delete_{name}(self): config = get_external_source_config("{name}") delete_{name}(logger=self.logger, config=config) - print('DELETE') """ + + class Command(BaseCommand): help = "Import services from City of Turku APIs and from external sources." # Umbrella source that imports all external_sources - MOBILITY_DATA = "mobility_data" + EXTERNAL_SOURCES = "external_sources" external_sources = get_configured_external_sources_names() importer_types = [ @@ -60,7 +60,7 @@ class Command(BaseCommand): "geo_search_addresses", "enriched_addresses", "divisions", - MOBILITY_DATA, + EXTERNAL_SOURCES, ] + external_sources supported_languages = [lang[0] for lang in settings.LANGUAGES] @@ -147,10 +147,10 @@ def import_divisions(self): return import_divisions(logger=self.logger) @db.transaction.atomic - def import_mobility_data(self): - # TODO fix this - self.import_bicycle_stands() - self.import_gas_filling_stations() + def import_external_sources(self): + for name in self.external_sources: + method = getattr(self, "import_%s" % name) + method() # Activate the default language for the duration of the import # to make sure translated fields are populated correctly. @@ -179,12 +179,12 @@ def handle(self, **options): sys.stderr.write("Nothing to delete.\n") else: importers = self.options["import_types"] - if self.MOBILITY_DATA in self.options["import_types"]: - # Add external sources and by creating a set ensure there are no duplicates - # as the user can add args as mobility_data charging_stations + if self.EXTERNAL_SOURCES in self.options["import_types"]: + # Add EXTERNAL_SOURCES by creating a set to ensure there are no duplicates + # as the user can add args as EXTERNAL_SOURCES charging_stations importers = set(importers + self.external_sources) - # remove the mobility_data source as it has no function attached to it. - importers.remove(self.MOBILITY_DATA) + # remove the EXTERNAL_SOURCES source as it has no function attached to it. + importers.remove(self.EXTERNAL_SOURCES) import_count = 0 for imp in self.importer_types: From 2211599f9e5c86b311cc87b181d98696ac512b17 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 10:43:19 +0200 Subject: [PATCH 084/188] Remove unnecessary param from create_service and create_service_node --- smbackend_turku/importers/utils.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/smbackend_turku/importers/utils.py b/smbackend_turku/importers/utils.py index ea399347d..8b7714eb4 100644 --- a/smbackend_turku/importers/utils.py +++ b/smbackend_turku/importers/utils.py @@ -290,19 +290,20 @@ def get_turku_boundary(): return None -def create_service_node(service_node_id, name, parent_name, service_node_names): +def create_service_node(service_node_id, parent_name, service_node_names): """ Creates service_node with given name and id if it does not exist. Sets the parent service_node and name fields. :param service_node_id: the id of the service_node to be created. - :param name: name of the service_node. :param parent_name: name of the parent service_node, if None the service_node will be topmost in the tree hierarchy. :param service_node_names: dict with names in all languages """ service_node = None try: - service_node = ServiceNode.objects.get(id=service_node_id, name=name) + service_node = ServiceNode.objects.get( + id=service_node_id, name=service_node_names["fi"] + ) except ServiceNode.DoesNotExist: service_node = ServiceNode(id=service_node_id) @@ -323,18 +324,17 @@ def create_service_node(service_node_id, name, parent_name, service_node_names): service_node.save() -def create_service(service_id, service_node_id, service_name, service_names): +def create_service(service_id, service_node_id, service_names): """ Creates service with given service_id and name if it does not exist. Adds the service to the given service_node and sets the name fields. :param service_id: the id of the service. :param service_node_id: the id of the service_node to which the service will have a relation - :param service_name: name of the service :param service_names: dict with names in all languages """ service = None try: - service = Service.objects.get(id=service_id, name=service_name) + service = Service.objects.get(id=service_id, name=service_names["fi"]) except Service.DoesNotExist: service = Service( id=service_id, clarification_enabled=False, period_enabled=False @@ -376,14 +376,12 @@ def __init__(self, config): self.delete_external_source() create_service_node( self.config["service_node"]["id"], - self.config["service_node"]["name"]["fi"], self.config["root_service_node_name"], self.config["service_node"]["name"], ) create_service( self.config["service"]["id"], self.config["service_node"]["id"], - self.config["service"]["name"]["fi"], self.config["service"]["name"], ) From 07080bc6107a5bfc2fa702f6851a12843390f22b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 13:17:41 +0200 Subject: [PATCH 085/188] Add info about importing playgrounds --- mobility_data/README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mobility_data/README.md b/mobility_data/README.md index 885571485..95fc03ad8 100644 --- a/mobility_data/README.md +++ b/mobility_data/README.md @@ -165,6 +165,12 @@ To import data type: ./manage.py import_wfs BarbecuePlace ``` + +### Playgrounds +``` +./manage.py import_wfs PlayGround +``` + ### Föli park and ride stop Imports park and ride stops for bikes and cars. ``` From 999f84d1d165169a4f1f898323871cc78ec00690 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 13:18:14 +0200 Subject: [PATCH 086/188] Add task that imports playgrounds --- mobility_data/tasks.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/mobility_data/tasks.py b/mobility_data/tasks.py index 9221d488d..3310bcc55 100644 --- a/mobility_data/tasks.py +++ b/mobility_data/tasks.py @@ -46,6 +46,11 @@ def import_barbecue_places(name="import_barbecue_places"): management.call_command("import_wfs", ["BarbecuePlace"]) +@shared_task +def import_playgrounds(name="import_playgrounds"): + management.call_command("import_wfs", ["PlayGround"]) + + @shared_task def import_share_car_parking_places(name="impor_share_car_parking_places"): management.call_command("import_share_car_parking_places") From 670b6ec254740a90f8a69293cb65e392a1283262 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 13:18:45 +0200 Subject: [PATCH 087/188] Add configuration for playgrounds --- .../importers/data/wfs_importer_config.yml | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/mobility_data/importers/data/wfs_importer_config.yml b/mobility_data/importers/data/wfs_importer_config.yml index 2201b566d..f86db8084 100644 --- a/mobility_data/importers/data/wfs_importer_config.yml +++ b/mobility_data/importers/data/wfs_importer_config.yml @@ -1,4 +1,40 @@ features: + - content_type_name: PlayGround + content_type_description: Playgrounds in the city of Turku. + wfs_layer: GIS:Viheralueet + max_features: 50000 + include: + Kayttotyyppi: Leikkipaikka + fields: + name: + fi: Tunnus + extra_fields: + kayttotyyppi: + wfs_field: Kayttotyyppi + omistaja: + wfs_field: Omistaja + haltija: + wfs_field: Haltija + kunnossapitaja: + wfs_field: Kunnossapitaja + hoitaja: + wfs_field: Hoitaja + alueUrakkaAlue: + wfs_field: AlueUrakkaAlue + kunnossapitoluokka: + wfs_field: Kunnossapitoluokka + talvikunnossapito: + wfs_field: Talvikunnossapito + pintamateriaali: + wfs_field: Pintamateriaali + laskettuPintaAla: + wfs_field: LaskettuPintaAla + wfs_type: double + valmistusvuosi: + wfs_field: Valmistusvuosi + peruskorjausvuosi: + wfs_field: Peruskorjausvuosi + - content_type_name: BarbecuePlace content_type_description: Barbecue places in the city of Turku. wfs_layer: GIS:Varusteet From 77d698e92d8f3e74089f45e140551726eb1f4717 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 14:23:40 +0200 Subject: [PATCH 088/188] Add support for multipointz --- mobility_data/importers/lounaistieto_shapefiles.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/importers/lounaistieto_shapefiles.py b/mobility_data/importers/lounaistieto_shapefiles.py index dec07aeb8..b99128a5e 100644 --- a/mobility_data/importers/lounaistieto_shapefiles.py +++ b/mobility_data/importers/lounaistieto_shapefiles.py @@ -50,7 +50,7 @@ def add_feature(self, feature, config, srid): match feature.shape.shapeTypeName: case "POLYLINE": geometry = LineString(feature.shape.points, srid=srid) - case "POINT": + case "POINT" | "MULTIPOINTZ": points = feature.shape.points[0] assert len(points) == 2 geometry = Point(points[0], points[1], srid=srid) From 55f20260920113ff88bd0a5001f881e71eee4792 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Feb 2023 14:25:31 +0200 Subject: [PATCH 089/188] Add config for bus stops in Southwest Finland --- .../data/lounaistieto_shapefiles_config.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml index c135b41a4..022bbf198 100644 --- a/mobility_data/importers/data/lounaistieto_shapefiles_config.yml +++ b/mobility_data/importers/data/lounaistieto_shapefiles_config.yml @@ -1,4 +1,23 @@ data_sources: + - content_type_name: BusStopSouthwestFinland + content_type_description: "Bus stops in Southwest Finland." + data_url: "https://data.lounaistieto.fi/data/dataset/ee440090-7303-4453-8639-b5f711669acb/resource/14060749-5215-41af-9312-5393c966f987/download/tieverkkodata.zip/bussipysakit.shp" + fields: + name: + fi: pysnimi + sv: stopnamn + extra_fields: + piiri: piiri + tie: tie + tiety: tiety + puoli: puoli + bussity: bussity + pikavuo: pikavuo + katos: katos + korotettu: korotettu + pysid: pysid + pystunn196: pystunn196 + - content_type_name: FerryDock content_type_description: "Ferry docks in Southwest Finland." data_url: "https://data.lounaistieto.fi/data/dataset/ea4dcae2-5832-403c-bf7e-b19783ee9a70/resource/faa02c52-2da8-4603-b8bf-4afca42d39a5/download/yhteysalusreitit_laiturit.zip/Laiturit.shp" From 6f169c62412f6e14045f6099e5a13590c9b1206b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 2 Feb 2023 10:27:28 +0200 Subject: [PATCH 090/188] Serialize sensor types for stations --- eco_counter/api/serializers.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/eco_counter/api/serializers.py b/eco_counter/api/serializers.py index 21d37b755..e0cbc80c8 100644 --- a/eco_counter/api/serializers.py +++ b/eco_counter/api/serializers.py @@ -31,6 +31,7 @@ class StationSerializer(serializers.ModelSerializer): y = serializers.SerializerMethodField() lon = serializers.SerializerMethodField() lat = serializers.SerializerMethodField() + sensor_types = serializers.SerializerMethodField() class Meta: model = Station @@ -47,6 +48,7 @@ class Meta: "y", "lon", "lat", + "sensor_types", ] def get_y(self, obj): @@ -63,6 +65,17 @@ def get_lon(self, obj): obj.geom.transform(4326) return obj.geom.x + def get_sensor_types(self, obj): + # Return the sensor types(car, bike etc) that has a total year value >0. + # i.e., there are sensors for counting the type of data. + types = ["at", "pt", "jt", "bt"] + result = [] + for type in types: + filter = {"station": obj, f"value_{type}__gt": 0} + if YearData.objects.filter(**filter).count() > 0: + result.append(type) + return result + class YearSerializer(serializers.ModelSerializer): station_name = serializers.PrimaryKeyRelatedField( From 21f0bd980b26980448453db0d254eeb16d606487 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 2 Feb 2023 10:27:56 +0200 Subject: [PATCH 091/188] Test station sensor types --- eco_counter/tests/test_api.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/eco_counter/tests/test_api.py b/eco_counter/tests/test_api.py index 3e870a5e8..4c10654fb 100644 --- a/eco_counter/tests/test_api.py +++ b/eco_counter/tests/test_api.py @@ -263,8 +263,9 @@ def test__months_multiple_years(api_client, years, test_timestamp): @pytest.mark.django_db -def test__station(api_client, station): +def test__station(api_client, station, year_datas): url = reverse("eco_counter:stations-list") response = api_client.get(url) assert response.status_code == 200 assert response.json()["results"][0]["name"] == station.name + assert response.json()["results"][0]["sensor_types"] == ["at"] From 4415aedc786b874b02e01519a919c287a6b0237c Mon Sep 17 00:00:00 2001 From: Juuso Jokiniemi <68938778+juuso-j@users.noreply.github.com> Date: Thu, 2 Feb 2023 11:13:54 +0200 Subject: [PATCH 092/188] Fix formating --- mobility_data/importers/gas_filling_station.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobility_data/importers/gas_filling_station.py b/mobility_data/importers/gas_filling_station.py index d3b122150..c00fda744 100644 --- a/mobility_data/importers/gas_filling_station.py +++ b/mobility_data/importers/gas_filling_station.py @@ -5,10 +5,9 @@ from django.contrib.gis.geos import Point, Polygon from munigeo.models import Municipality - from mobility_data.models import MobileUnit -from .constants import SOUTHWEST_FINLAND_GEOMETRY +from .constants import SOUTHWEST_FINLAND_BOUNDARY, SOUTHWEST_FINLAND_BOUNDARY_SRID from .utils import ( delete_mobile_units, fetch_json, From 8f57fe6c846d01827ccd5c666109086e420bba72 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 6 Feb 2023 12:30:35 +0200 Subject: [PATCH 093/188] Replace @shared_task with @shared_task_email decorator --- eco_counter/tasks.py | 7 +++--- iot/tasks.py | 5 ++-- mobility_data/tasks.py | 51 ++++++++++++++++++++-------------------- ptv/tasks.py | 5 ++-- smbackend_turku/tasks.py | 33 +++++++++++++------------- 5 files changed, 53 insertions(+), 48 deletions(-) diff --git a/eco_counter/tasks.py b/eco_counter/tasks.py index dd0e9a722..9b7fdb714 100644 --- a/eco_counter/tasks.py +++ b/eco_counter/tasks.py @@ -1,12 +1,13 @@ -from celery import shared_task from django.core import management +from smbackend.utils import shared_task_email -@shared_task + +@shared_task_email def import_counter_data(args, name="import_counter_data"): management.call_command("import_counter_data", "--counters", args) -@shared_task +@shared_task_email def initial_import_counter_data(args, name="initial_import_counter_data"): management.call_command("import_counter_data", "--init", args) diff --git a/iot/tasks.py b/iot/tasks.py index 7df693045..a4b7fd7d2 100644 --- a/iot/tasks.py +++ b/iot/tasks.py @@ -1,7 +1,8 @@ -from celery import shared_task from django.core import management +from smbackend.utils import shared_task_email -@shared_task + +@shared_task_email def import_iot_data(source_name, name="Import iot data"): management.call_command("import_iot_data", source_name) diff --git a/mobility_data/tasks.py b/mobility_data/tasks.py index 3310bcc55..9ad42bb02 100644 --- a/mobility_data/tasks.py +++ b/mobility_data/tasks.py @@ -1,8 +1,9 @@ -from celery import shared_task from django.core import management +from smbackend.utils import shared_task_email -@shared_task + +@shared_task_email def import_culture_routes(args=None, name="import_culture_routes"): if args: management.call_command("import_culture_routes", args) @@ -10,17 +11,17 @@ def import_culture_routes(args=None, name="import_culture_routes"): management.call_command("import_culture_routes") -@shared_task +@shared_task_email def import_payments_zones(name="import_payment_zones"): management.call_command("import_wfs", "PaymentZone") -@shared_task +@shared_task_email def import_speed_limit_zones(name="import_speed_limit_zones"): management.call_command("import_wfs", "SpeedLimitZone") -@shared_task +@shared_task_email def import_scooter_restrictions(name="import_scooter_restrictions"): management.call_command( "import_wfs", @@ -28,12 +29,12 @@ def import_scooter_restrictions(name="import_scooter_restrictions"): ) -@shared_task +@shared_task_email def import_mobility_data(name="import_mobility_data"): management.call_command("import_mobility_data") -@shared_task +@shared_task_email def import_accessories(name="import_accessories"): management.call_command( "import_wfs", @@ -41,81 +42,81 @@ def import_accessories(name="import_accessories"): ) -@shared_task +@shared_task_email def import_barbecue_places(name="import_barbecue_places"): management.call_command("import_wfs", ["BarbecuePlace"]) -@shared_task +@shared_task_email def import_playgrounds(name="import_playgrounds"): management.call_command("import_wfs", ["PlayGround"]) -@shared_task +@shared_task_email def import_share_car_parking_places(name="impor_share_car_parking_places"): management.call_command("import_share_car_parking_places") -@shared_task +@shared_task_email def import_bicycle_networks(name="import_bicycle_networks"): management.call_command( "import_wfs", ["BrushSaltedBicycleNetwork", "BrushSandedBicycleNetwork"] ) -@shared_task +@shared_task_email def import_marinas(name="import_marinas"): management.call_command("import_marinas") -@shared_task +@shared_task_email def import_foli_stops(name="import_foli_stops"): management.call_command("import_foli_stops") -@shared_task +@shared_task_email def import_foli_parkandride_stops(name="import_foli_parkandride_stops"): management.call_command("import_foli_parkandride_stops") -@shared_task +@shared_task_email def import_outdoor_gym_devices(name="import_outdoor_gym_devices"): management.call_command("import_outdoor_gym_devices") -@shared_task +@shared_task_email def import_disabled_and_no_staff_parkings(name="import_disabled_and_no_staff_parkings"): management.call_command("import_disabled_and_no_staff_parkings") -@shared_task +@shared_task_email def import_loading_and_unloading_places(name="import_loading_and_unloading_places"): management.call_command("import_loading_and_unloading_places") -@shared_task +@shared_task_email def import_lounaistieto_shapefiles(name="import_lounaistieto_shapefiles"): management.call_command("import_lounaistieto_shapefiles") -@shared_task +@shared_task_email def import_paavonpolkus(name="import_paavonpolkus"): management.call_command("import_wfs", "PaavonPolku") -@shared_task +@shared_task_email def delete_mobility_data(args=None, name="delete_mobility_data"): management.call_command("delete_mobility_data", args) -@shared_task +@shared_task_email def import_outdoor_trails(name="import_outdoor_trails"): management.call_command( "import_wfs", ["PaddlingTrail", "HikingTrail", "NatureTrail", "FitnessTrail"] ) -@shared_task +@shared_task_email def import_traffic_signs(name="import_traffic_signs"): management.call_command( "import_wfs", @@ -139,16 +140,16 @@ def import_traffic_signs(name="import_traffic_signs"): ) -@shared_task +@shared_task_email def import_wfs(args=None, name="import_wfs"): management.call_command("import_wfs", args) -@shared_task +@shared_task_email def import_parking_machines(name="import_parking_machines"): management.call_command("import_parking_machines") -@shared_task +@shared_task_email def delete_deprecated_units(name="delete_deprecated_units"): management.call_command("delete_deprecated_units") diff --git a/ptv/tasks.py b/ptv/tasks.py index 1e07c1789..872902026 100644 --- a/ptv/tasks.py +++ b/ptv/tasks.py @@ -1,8 +1,9 @@ -from celery import shared_task from django.core import management +from smbackend.utils import shared_task_email -@shared_task + +@shared_task_email def import_ptv_data(name="import_ptv_data"): # Note, Aura=19 has been removed, thus it is not found in palvelutietovaranto. management.call_command( diff --git a/smbackend_turku/tasks.py b/smbackend_turku/tasks.py index 7fef1c361..a36ab80eb 100644 --- a/smbackend_turku/tasks.py +++ b/smbackend_turku/tasks.py @@ -1,30 +1,31 @@ -from celery import shared_task from django.core import management +from smbackend.utils import shared_task_email -@shared_task + +@shared_task_email def turku_services_import(args, name="turku_services_import"): management.call_command("turku_services_import", args) -@shared_task +@shared_task_email def delete_external_source(source, name="delete_external_source"): management.call_command("turku_services_import", source, "--delete-external-source") -@shared_task +@shared_task_email def import_mds_data(name="import_mds_data"): management.call_command( "turku_services_import", "services", "accessibility", "units" ) -@shared_task +@shared_task_email def import_division(name="import_division"): management.call_command("turku_services_import", "divisions") -@shared_task +@shared_task_email def import_all_addresses(name="import_all_addresses"): # Task that imports all the addresses and indexes search columns management.call_command("turku_services_import", "addresses") @@ -33,54 +34,54 @@ def import_all_addresses(name="import_all_addresses"): management.call_command("index_search_columns") -@shared_task +@shared_task_email def import_addresses(name="import_addresses"): # Imports addresses for Turku and Karina from the WFS server hosted by Turku management.call_command("turku_services_import", "addresses") -@shared_task +@shared_task_email def geo_import_municipalities(name="geo_import_municipalities"): management.call_command("geo_import", "finland", "--municipalities") -@shared_task +@shared_task_email def index_search_columns(name="index_search_columns"): management.call_command("index_search_columns") -@shared_task +@shared_task_email def import_geo_search_addresses(name="import_geo_search_addresses"): # Imports the addresses of Southwest Finland(not Turku and Kaarina) from geo-search(paikkatietohaku) management.call_command("turku_services_import", "geo_search_addresses") -@shared_task +@shared_task_email def import_enriched_addresses(name="import_enriched_addresses"): # Enriches addresses for Turku and Karina from geo-search(paikkatietohaku) management.call_command("turku_services_import", "enriched_addresses") -@shared_task +@shared_task_email def import_bicycle_stands(name="import_bicycle_stands"): management.call_command("turku_services_import", "bicycle_stands") -@shared_task +@shared_task_email def import_bike_service_stations(name="bike_service_stations"): management.call_command("turku_services_import", "bike_service_stations") -@shared_task +@shared_task_email def import_gas_filling_stations(name="import_gas_filling_stations"): management.call_command("turku_services_import", "gas_filling_stations") -@shared_task +@shared_task_email def import_charging_stations(name="import_charging_stations"): management.call_command("turku_services_import", "charging_stations") -@shared_task +@shared_task_email def import_external_sources(name="import_external_sources"): management.call_command("turku_services_import", "external_sources") From d58140cf14dd28a6c8986f87d9001d9b8609aac4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 6 Feb 2023 12:31:20 +0200 Subject: [PATCH 094/188] Add email setting and celery admin group --- smbackend/settings.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/smbackend/settings.py b/smbackend/settings.py index 09f9b2580..bfec5ef03 100644 --- a/smbackend/settings.py +++ b/smbackend/settings.py @@ -217,6 +217,11 @@ def gettext(s): # Shortcut generation URL template SHORTCUTTER_UNIT_URL = env("SHORTCUTTER_UNIT_URL") +EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" +EMAIL_HOST = "smtp.turku.fi" +EMAIL_HOST_USER = "varaamo@turku.fi" +EMAIL_PORT = 25 +EMAIL_USE_TLS = True # Static & Media files STATIC_ROOT = env("STATIC_ROOT") STATIC_URL = env("STATIC_URL") @@ -332,6 +337,9 @@ def preprocessing_filter_spec(endpoints): CELERY_CACHE_BACKEND = "default" CELERY_BEAT_SCHEDULER = "django_celery_beat.schedulers:DatabaseScheduler" CELERY_CACHE_BACKEND = "django-cache" +# User in this group will be notified with failed tasks. +CELERY_ADMIN_GROUP = "CeleryAdmin" + CACHES = { "default": { From 12e1710d22da436e2bc03331d28009fabc33ed4b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 6 Feb 2023 12:31:55 +0200 Subject: [PATCH 095/188] Add function(decorator) that sends email if and exception is raised in wrapped function --- smbackend/utils.py | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 smbackend/utils.py diff --git a/smbackend/utils.py b/smbackend/utils.py new file mode 100644 index 000000000..474d0e3ec --- /dev/null +++ b/smbackend/utils.py @@ -0,0 +1,45 @@ +import traceback +from functools import wraps + +from celery import shared_task +from django.conf import settings +from django.contrib.auth.models import User +from django.core.mail import send_mail + + +def get_emails_to_notify(): + """ + Return email addresses for users in group 'settings.CELERY_ADMIN_GROUP'. + """ + return [ + user.email + for user in User.objects.filter(groups__name=settings.CELERY_ADMIN_GROUP) + ] + + +def shared_task_email(func): + """ + Replacement for @shared_task decorator that emails users in group settins.CELERY_ADMIN_GROUP + if an exception is raised. + """ + + @wraps(func) + def new_func(*args, **kwargs): + try: + return func(*args, **kwargs) + except Exception: + notify_emails = get_emails_to_notify() + if len(notify_emails) > 0: + subject = "Celery task failure" + message = f"Function: {str(func)} \n" + message += traceback.format_exc() + send_mail( + subject, + message, + "varaamo@turku.fi", + notify_emails, + fail_silently=False, + ) + raise + + return shared_task(new_func) From 84d7a2af1834e2c1eef06cc9472f3f316a40e4c0 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 6 Feb 2023 13:50:37 +0200 Subject: [PATCH 096/188] Add swedish names to major_districts --- smbackend_turku/importers/data/divisions_config.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/smbackend_turku/importers/data/divisions_config.yml b/smbackend_turku/importers/data/divisions_config.yml index 1e0f04961..813ec08fa 100644 --- a/smbackend_turku/importers/data/divisions_config.yml +++ b/smbackend_turku/importers/data/divisions_config.yml @@ -11,9 +11,10 @@ divisions: wfs_layer: 'GIS:Palveluverkko_suuralueet' fields: name: - fi: Tunnus - origin_id: Tunnus - ocd_id: Tunnus + fi: Tunnus_FIN + sv: Tunnus_FIN + origin_id: Tunnus_FIN + ocd_id: Tunnus_FIN - type: district name: Kaupunginosa From b9f31ad62568a8fc4fd06e845e59326b62409e89 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 6 Feb 2023 13:57:16 +0200 Subject: [PATCH 097/188] Add swedish names districts --- .../importers/data/divisions_config.yml | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/smbackend_turku/importers/data/divisions_config.yml b/smbackend_turku/importers/data/divisions_config.yml index 813ec08fa..2caec23ee 100644 --- a/smbackend_turku/importers/data/divisions_config.yml +++ b/smbackend_turku/importers/data/divisions_config.yml @@ -23,9 +23,10 @@ divisions: wfs_layer: 'GIS:Kaupunginosat' fields: name: - fi: Kaupunginosan_Nimi - origin_id: Kaupunginosan_Nimi - ocd_id: Kaupunginosan_Nimi + fi: nimi_FIN + sv: nimi_SVE + origin_id: nimi_FIN + ocd_id: nimi_FIN - type: sub_district name: Pienalue @@ -35,6 +36,8 @@ divisions: fields: name: fi: Numero + sv: Numero + en: Numero origin_id: Numero ocd_id: Numero @@ -56,7 +59,9 @@ divisions: check_turku_boundary: False fields: name: - fi: Tunnus + fi: Tunnus + sv: Tunnus + en: Tunnus origin_id: Tunnus ocd_id: Tunnus @@ -89,14 +94,4 @@ divisions: sv: Oppilasalueen_kuvaus origin_id: Oppilasalueen_kuvaus ocd_id: Oppilasalueen_kuvaus - - - type: parish - name: "Seurakunta" - ocd_id: seurakunta - wfs_layer: 'GIS:Seurakunnat' - fields: - name: - fi: Numero - origin_id: Numero - ocd_id: Numero - + From 3bb88c77a6a8ddb565516e9b93bbee4e97a9ca05 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 09:50:43 +0200 Subject: [PATCH 098/188] Remove useless filter --- bicycle_network/api/views.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/bicycle_network/api/views.py b/bicycle_network/api/views.py index 7fbd440e0..d7bb73423 100644 --- a/bicycle_network/api/views.py +++ b/bicycle_network/api/views.py @@ -82,11 +82,13 @@ def list(self, request): ) # Return elements that are inside bbox if "bbox" in filters: - ref = SpatialReference(4326) - val = self.request.query_params.get("bbox", None) - bbox_filter = munigeo_api.build_bbox_filter(ref, val, "geometry") - bbox_geometry_filter = munigeo_api.build_bbox_filter(ref, val, "geometry") - queryset = queryset.filter(Q(**bbox_filter) | Q(**bbox_geometry_filter)) + val = filters.get("bbox", None) + if val: + ref = SpatialReference(filters.get("bbox_srid", 4326)) + bbox_geometry_filter = munigeo_api.build_bbox_filter( + ref, val, "geometry" + ) + queryset = queryset.filter(Q(**bbox_geometry_filter)) page = self.paginate_queryset(queryset) if only_coords: From 7257a6d860ab52111dc850ef0b278f641636841e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 09:51:02 +0200 Subject: [PATCH 099/188] Add bbox and bbox_srid param --- mobility_data/specification.swagger2.0.yaml | 26 ++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/mobility_data/specification.swagger2.0.yaml b/mobility_data/specification.swagger2.0.yaml index 832ad84da..66ca0652b 100644 --- a/mobility_data/specification.swagger2.0.yaml +++ b/mobility_data/specification.swagger2.0.yaml @@ -134,6 +134,9 @@ paths: - $ref: "#/components/parameters/latlon_param" - $ref: "#/components/parameters/type_name_param" - $ref: "#/components/parameters/extra_param" + - $ref: "#/components/parameters/bbox_param" + - $ref: "#/components/parameters/bbox_srid_param" + responses: 200: description: "List of MobileUnits." @@ -286,4 +289,25 @@ components: required: false schema: type: string - example: extra__fieldname=value \ No newline at end of file + example: extra__fieldname=value + + bbox_param: + name: bbox + in: query + description: Search for mobile units that are within this bounding box. Decimal coordinates + are given in order west, south, east, north. Period is used as decimal + separator. Default srid is 4326. + schema: + type: array + items: + type: number + example: 24.9405559,60.1695096,24.9805559,60.1895096 + + bbox_srid_param: + name: bbox_srid + in: query + description: An SRID coordinate reference system identifier which specifies the + coordinate system used in the bbox parameter. + schema: + type: integer + example: 3046 \ No newline at end of file From 9632945d5ce768044206356f94bc4d70a0f41adb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 09:51:58 +0200 Subject: [PATCH 100/188] Fix mobile unit geometry srid --- mobility_data/tests/conftest.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mobility_data/tests/conftest.py b/mobility_data/tests/conftest.py index 712225228..d297c0907 100644 --- a/mobility_data/tests/conftest.py +++ b/mobility_data/tests/conftest.py @@ -66,11 +66,13 @@ def group_type(): @pytest.fixture def mobile_unit(content_type): extra = {"test_int": 4242, "test_float": 42.42, "test_string": "4242"} + geometry = Point(22.21, 60.3, srid=4326) + geometry.transform(settings.DEFAULT_SRID) mobile_unit = MobileUnit.objects.create( name="Test mobileunit", description="Test description", content_type=content_type, - geometry=Point(42.42, 21.21, srid=settings.DEFAULT_SRID), + geometry=geometry, extra=extra, ) return mobile_unit From 0cb0d25ca74e2b54fcbeff39cb6f7f402f710030 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 09:53:11 +0200 Subject: [PATCH 101/188] Add bbox param --- mobility_data/api/views.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/mobility_data/api/views.py b/mobility_data/api/views.py index 5d671cd23..de353e99d 100644 --- a/mobility_data/api/views.py +++ b/mobility_data/api/views.py @@ -1,6 +1,9 @@ from distutils.util import strtobool +from django.contrib.gis.gdal import SpatialReference from django.core.exceptions import ValidationError +from django.db.models import Q +from munigeo import api as munigeo_api from rest_framework import status, viewsets from rest_framework.exceptions import ParseError from rest_framework.response import Response @@ -191,6 +194,15 @@ def list(self, request): else: queryset = MobileUnit.objects.all() + if "bbox" in filters: + val = filters.get("bbox", None) + if val: + ref = SpatialReference(filters.get("bbox_srid", 4326)) + bbox_geometry_filter = munigeo_api.build_bbox_filter( + ref, val, "geometry" + ) + queryset = queryset.filter(Q(**bbox_geometry_filter)) + for filter in filters: if filter.startswith("extra__"): if "type_name" not in filters: From 8707f61c96bd5bda27cc6bbd44d11fa0be966c7b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 09:54:54 +0200 Subject: [PATCH 102/188] Add bbox param tests --- mobility_data/tests/test_api.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index a8cb72f46..7f9729583 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -36,7 +36,9 @@ def test_mobile_unit(api_client, mobile_unit, content_type): assert results["extra"]["test_string"] == "4242" assert results["extra"]["test_int"] == 4242 assert results["extra"]["test_float"] == 42.42 - assert results["geometry"] == Point(42.42, 21.21, srid=settings.DEFAULT_SRID) + assert results["geometry"] == Point( + 235404.6706163187, 6694437.919005549, srid=settings.DEFAULT_SRID + ) url = ( reverse("mobility_data:mobile_units-list") + "?type_name=Test?extra__test_string=4242" @@ -63,6 +65,17 @@ def test_mobile_unit(api_client, mobile_unit, content_type): assert response.status_code == 200 results = response.json()["results"][0] assert results["name"] == "Test mobileunit" + # Test that we get a mobile unit inside bbox. + url = ( + reverse("mobility_data:mobile_units-list") + + "?bbox=21.1,59.2,22.3,61.4&bbox_srid=4326" + ) + response = api_client.get(url) + assert len(response.json()["results"]) == 1 + # Test bbox where no mobile units are inside. + url = reverse("mobility_data:mobile_units-list") + "?bbox=22.1,60.2,2.3,60.4" + response = api_client.get(url) + assert len(response.json()["results"]) == 0 @pytest.mark.django_db From 3a82e25520b1f42953a7639d116e79573fff7063 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 10:45:53 +0200 Subject: [PATCH 103/188] Add email example env variables --- config_dev.env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/config_dev.env.example b/config_dev.env.example index 01f85a62a..c7b6241cb 100644 --- a/config_dev.env.example +++ b/config_dev.env.example @@ -138,6 +138,13 @@ CELERY_BROKER_URL=redis://localhost:6379/0 # Cache location, e.g. redis on localhost using default port and database 0 CACHE_LOCATION=redis://localhost:6379/0 +# Email settings +EMAIL_BACKEND=django.core.mail.backends.smtp.EmailBackend +EMAIL_HOST=smtp.example.com +EMAIL_HOST_USER=example@example.com +EMAIL_PORT=25 +EMAIL_USE_TLS=True + # Settings needed for enabling Turku area: #ADDITIONAL_INSTALLED_APPS=smbackend_turku,ptv #TURKU_API_KEY=secret From 0337bd021bfd4c1571fd15a90314f8e62c2d02e2 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 10:46:30 +0200 Subject: [PATCH 104/188] Read email settings from env --- smbackend/settings.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/smbackend/settings.py b/smbackend/settings.py index bfec5ef03..0e786c728 100644 --- a/smbackend/settings.py +++ b/smbackend/settings.py @@ -57,6 +57,11 @@ YIT_CONTRACTS_URL=(str, None), YIT_TOKEN_URL=(str, None), KUNTEC_KEY=(str, None), + EMAIL_BACKEND=(str, None), + EMAIL_HOST=(str, None), + EMAIL_HOST_USER=(str, None), + EMAIL_PORT=(int, None), + EMAIL_USE_TLS=(bool, None), ) # Build paths inside the project like this: os.path.join(BASE_DIR, ...) @@ -217,11 +222,13 @@ def gettext(s): # Shortcut generation URL template SHORTCUTTER_UNIT_URL = env("SHORTCUTTER_UNIT_URL") -EMAIL_BACKEND = "django.core.mail.backends.smtp.EmailBackend" -EMAIL_HOST = "smtp.turku.fi" -EMAIL_HOST_USER = "varaamo@turku.fi" -EMAIL_PORT = 25 -EMAIL_USE_TLS = True + +EMAIL_BACKEND = env("EMAIL_BACKEND") +EMAIL_HOST = env("EMAIL_HOST") +EMAIL_HOST_USER = env("EMAIL_HOST_USER") +EMAIL_PORT = env("EMAIL_PORT") +EMAIL_USE_TLS = env("EMAIL_USE_TLS") + # Static & Media files STATIC_ROOT = env("STATIC_ROOT") STATIC_URL = env("STATIC_URL") From c21da4d6c650fd4edb2a21531afefd1ea4e118cb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Feb 2023 10:46:57 +0200 Subject: [PATCH 105/188] Read sender from settings.EMAIL_HOST_USER --- smbackend/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smbackend/utils.py b/smbackend/utils.py index 474d0e3ec..413c56877 100644 --- a/smbackend/utils.py +++ b/smbackend/utils.py @@ -36,7 +36,7 @@ def new_func(*args, **kwargs): send_mail( subject, message, - "varaamo@turku.fi", + settings.EMAIL_HOST_USER, notify_emails, fail_silently=False, ) From 02d7175c4b114b9eafba64aad259834f686424d2 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:35:56 +0200 Subject: [PATCH 106/188] Change content_type to content_types --- mobility_data/specification.swagger2.0.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/specification.swagger2.0.yaml b/mobility_data/specification.swagger2.0.yaml index 832ad84da..6a4bc0350 100644 --- a/mobility_data/specification.swagger2.0.yaml +++ b/mobility_data/specification.swagger2.0.yaml @@ -60,7 +60,7 @@ definitions: type: string description_en: type: string - content_type: + content_types: $ref: "#/definitions/content_type" mobile_unit_group: $ref: "#/definitions/mobile_unit_group" From 2c48fdfc807bfd27ae15f4b0387dfd3622e2e966 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:37:17 +0200 Subject: [PATCH 107/188] Filter with content_types field --- mobility_data/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/api/views.py b/mobility_data/api/views.py index 5d671cd23..d471000ee 100644 --- a/mobility_data/api/views.py +++ b/mobility_data/api/views.py @@ -187,7 +187,7 @@ def list(self, request): return Response( "type_name does not exist.", status=status.HTTP_400_BAD_REQUEST ) - queryset = MobileUnit.objects.filter(content_type__name=type_name) + queryset = MobileUnit.objects.filter(content_types__name=type_name) else: queryset = MobileUnit.objects.all() From 4fedcc2254c144db2778bec8ab6cb7bebc1759a1 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:38:09 +0200 Subject: [PATCH 108/188] Change content_type field to content_types --- mobility_data/api/serializers/mobile_unit.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/mobility_data/api/serializers/mobile_unit.py b/mobility_data/api/serializers/mobile_unit.py index a88b09c85..f4ebe60e4 100644 --- a/mobility_data/api/serializers/mobile_unit.py +++ b/mobility_data/api/serializers/mobile_unit.py @@ -42,7 +42,7 @@ class Meta: class MobileUnitSerializer(serializers.ModelSerializer): - content_type = ContentTypeSerializer(many=False, read_only=True) + content_types = ContentTypeSerializer(many=True, read_only=True) mobile_unit_group = MobileUnitGroupBasicInfoSerializer(many=False, read_only=True) geometry_coords = serializers.SerializerMethodField(read_only=True) @@ -64,7 +64,7 @@ class Meta: "description_fi", "description_sv", "description_en", - "content_type", + "content_types", "mobile_unit_group", "is_active", "created_time", @@ -133,7 +133,6 @@ def get_geometry_coords(self, obj): pos["lon"] = geometry.x pos["lat"] = geometry.y return pos - elif isinstance(geometry, LineString): if self.context["latlon"]: # Return LineString coordinates in (lat,lon) format @@ -145,7 +144,6 @@ def get_geometry_coords(self, obj): return coords else: return geometry.coords - elif isinstance(geometry, Polygon): if self.context["latlon"]: # Return Polygon coordinates in (lat,lon) format @@ -192,6 +190,5 @@ def get_geometry_coords(self, obj): return coords else: return geometry.coords - else: return "" From accb96b5f5ebc620f86b982829b78ecbe45d953f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:44:46 +0200 Subject: [PATCH 109/188] Migration that adds content_types field to MobileUnit Add migration that adds field content_types to MobileUnit --- ...many_field_content_types_to_mobile_unit.py | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 mobility_data/migrations/0035_add_many_to_many_field_content_types_to_mobile_unit.py diff --git a/mobility_data/migrations/0035_add_many_to_many_field_content_types_to_mobile_unit.py b/mobility_data/migrations/0035_add_many_to_many_field_content_types_to_mobile_unit.py new file mode 100644 index 000000000..029d6b23f --- /dev/null +++ b/mobility_data/migrations/0035_add_many_to_many_field_content_types_to_mobile_unit.py @@ -0,0 +1,20 @@ +# Generated by Django 4.1.2 on 2023-02-08 07:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("mobility_data", "0034_remove_grouptype_type_name"), + ] + + operations = [ + migrations.AddField( + model_name="mobileunit", + name="content_types", + field=models.ManyToManyField( + related_name="mobile_units", to="mobility_data.contenttype" + ), + ), + ] From 89e70a86d28c06d4e0cc9ca2247c7fe0ec8ed214 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:49:37 +0200 Subject: [PATCH 110/188] Add data migration that populates content_types for MobileUnits --- ...0036_populate_mobile_type_content_types.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 mobility_data/migrations/0036_populate_mobile_type_content_types.py diff --git a/mobility_data/migrations/0036_populate_mobile_type_content_types.py b/mobility_data/migrations/0036_populate_mobile_type_content_types.py new file mode 100644 index 000000000..cafd7c797 --- /dev/null +++ b/mobility_data/migrations/0036_populate_mobile_type_content_types.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.2 on 2023-02-08 07:12 + +from django.db import migrations + + +def make_many_to_many_content_types(apps, schema_editor): + MobileUnit = apps.get_model("mobility_data", "MobileUnit") + + for mobile_unit in MobileUnit.objects.all(): + mobile_unit.content_types.add(mobile_unit.content_type) + + +class Migration(migrations.Migration): + + dependencies = [ + ("mobility_data", "0035_add_many_to_many_field_content_types_to_mobile_unit"), + ] + + operations = [ + migrations.RunPython(make_many_to_many_content_types), + ] From 317ea7af60e93fdcc5ed9704f91b27d515b45550 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:50:42 +0200 Subject: [PATCH 111/188] Add migration that removes content_type field for MobileUnit --- .../0037_remove_mobileunit_content_type.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 mobility_data/migrations/0037_remove_mobileunit_content_type.py diff --git a/mobility_data/migrations/0037_remove_mobileunit_content_type.py b/mobility_data/migrations/0037_remove_mobileunit_content_type.py new file mode 100644 index 000000000..8d0626c0a --- /dev/null +++ b/mobility_data/migrations/0037_remove_mobileunit_content_type.py @@ -0,0 +1,17 @@ +# Generated by Django 4.1.2 on 2023-02-08 08:57 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("mobility_data", "0036_populate_mobile_type_content_types"), + ] + + operations = [ + migrations.RemoveField( + model_name="mobileunit", + name="content_type", + ), + ] From 580630113b8db04362390ffe411d3d16d71ff612 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:52:07 +0200 Subject: [PATCH 112/188] Add migration that sets ContentType and GroupType ordering by name --- ...pe_and_grouptype_ordering_to_field_name.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 mobility_data/migrations/0038_alter_contenttype_and_grouptype_ordering_to_field_name.py diff --git a/mobility_data/migrations/0038_alter_contenttype_and_grouptype_ordering_to_field_name.py b/mobility_data/migrations/0038_alter_contenttype_and_grouptype_ordering_to_field_name.py new file mode 100644 index 000000000..2d450fe7f --- /dev/null +++ b/mobility_data/migrations/0038_alter_contenttype_and_grouptype_ordering_to_field_name.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.2 on 2023-02-08 14:04 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("mobility_data", "0037_remove_mobileunit_content_type"), + ] + + operations = [ + migrations.AlterModelOptions( + name="contenttype", + options={"ordering": ["name"]}, + ), + migrations.AlterModelOptions( + name="grouptype", + options={"ordering": ["name"]}, + ), + ] From efea6088020bb489f6dc3e35b78c53f32533f558 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:55:43 +0200 Subject: [PATCH 113/188] Add content_type to content_types m2m relationship --- mobility_data/importers/bicycle_stands.py | 3 +-- mobility_data/importers/bike_service_stations.py | 2 +- mobility_data/importers/charging_stations.py | 2 +- mobility_data/importers/culture_routes.py | 2 +- .../importers/disabled_and_no_staff_parking.py | 16 ++++++---------- mobility_data/importers/foli_parkandride_stop.py | 2 +- mobility_data/importers/foli_stops.py | 4 ++-- mobility_data/importers/gas_filling_station.py | 2 +- .../importers/loading_unloading_places.py | 2 +- .../importers/lounaistieto_shapefiles.py | 3 ++- mobility_data/importers/marinas.py | 7 ++++--- mobility_data/importers/outdoor_gym_devices.py | 4 +++- mobility_data/importers/parking_machines.py | 4 ++-- .../importers/share_car_parking_places.py | 5 ++--- mobility_data/importers/utils.py | 4 ++-- mobility_data/importers/wfs.py | 3 ++- 16 files changed, 32 insertions(+), 33 deletions(-) diff --git a/mobility_data/importers/bicycle_stands.py b/mobility_data/importers/bicycle_stands.py index 99f1888dc..967721b49 100644 --- a/mobility_data/importers/bicycle_stands.py +++ b/mobility_data/importers/bicycle_stands.py @@ -275,11 +275,10 @@ def save_to_database(objects, delete_tables=True): content_type = create_bicycle_stand_content_type() for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, municipality=object.municipality, ) - + mobile_unit.content_types.add(content_type) mobile_unit.geometry = object.geometry set_translated_field(mobile_unit, "name", object.name) if object.address: diff --git a/mobility_data/importers/bike_service_stations.py b/mobility_data/importers/bike_service_stations.py index ce0f10acc..f7d00ad9a 100644 --- a/mobility_data/importers/bike_service_stations.py +++ b/mobility_data/importers/bike_service_stations.py @@ -110,11 +110,11 @@ def save_to_database(objects, delete_tables=True): content_type = create_bike_service_station_content_type() for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, geometry=object.geometry, address_zip=object.address_zip, ) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "description", object.description) set_translated_field(mobile_unit, "address", object.address) diff --git a/mobility_data/importers/charging_stations.py b/mobility_data/importers/charging_stations.py index 1d0badaa5..be6a5f36b 100644 --- a/mobility_data/importers/charging_stations.py +++ b/mobility_data/importers/charging_stations.py @@ -187,9 +187,9 @@ def save_to_database(objects, delete_tables=True): is_active=is_active, geometry=object.geometry, extra=object.extra, - content_type=content_type, address_zip=object.address_zip, ) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() diff --git a/mobility_data/importers/culture_routes.py b/mobility_data/importers/culture_routes.py index 6d37ae0dd..0829b9395 100644 --- a/mobility_data/importers/culture_routes.py +++ b/mobility_data/importers/culture_routes.py @@ -290,10 +290,10 @@ def save_to_database(routes, delete_tables=False): content_type = geometry_type mobile_unit, created = MobileUnit.objects.get_or_create( - content_type=content_type, mobile_unit_group=group, geometry=placemark.geometry, ) + mobile_unit.content_types.add(content_type) if created: mobile_unit.is_active = True set_translated_field(mobile_unit, "name", placemark.name) diff --git a/mobility_data/importers/disabled_and_no_staff_parking.py b/mobility_data/importers/disabled_and_no_staff_parking.py index 90204a546..ff9cfdb0e 100644 --- a/mobility_data/importers/disabled_and_no_staff_parking.py +++ b/mobility_data/importers/disabled_and_no_staff_parking.py @@ -196,18 +196,14 @@ def save_to_database(objects, delete_tables=True): disabled_parking_content_type = get_and_create_disabled_parking_content_type() for object in objects: + mobile_unit = MobileUnit.objects.create( + extra=object.extra, + geometry=object.geometry, + ) if object.content_type == NO_STAFF_PARKING_CONTENT_TYPE_NAME: - mobile_unit = MobileUnit.objects.create( - content_type=no_staff_parking_content_type, - extra=object.extra, - geometry=object.geometry, - ) + mobile_unit.content_types.add(no_staff_parking_content_type) else: - mobile_unit = MobileUnit.objects.create( - content_type=disabled_parking_content_type, - extra=object.extra, - geometry=object.geometry, - ) + mobile_unit.content_types.add(disabled_parking_content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.address_zip = object.address_zip diff --git a/mobility_data/importers/foli_parkandride_stop.py b/mobility_data/importers/foli_parkandride_stop.py index 048b3d4e4..b1ff60df4 100644 --- a/mobility_data/importers/foli_parkandride_stop.py +++ b/mobility_data/importers/foli_parkandride_stop.py @@ -95,12 +95,12 @@ def save_to_database(objects, content_type_name, delete_tables=True): for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, geometry=object.geometry, address_zip=object.address_zip, description=object.description, municipality=object.municipality, ) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() diff --git a/mobility_data/importers/foli_stops.py b/mobility_data/importers/foli_stops.py index aa5d8b769..a86a25cef 100644 --- a/mobility_data/importers/foli_stops.py +++ b/mobility_data/importers/foli_stops.py @@ -46,10 +46,10 @@ def save_to_database(objects, delete_tables=True): content_type = get_and_create_foli_stop_content_type() for object in objects: - MobileUnit.objects.create( - content_type=content_type, + mobile_unit = MobileUnit.objects.create( name=object.name, geometry=object.geometry, extra=object.extra, ) + mobile_unit.content_types.add(content_type) logger.info(f"Saved {len(objects)} Föli stops to database.") diff --git a/mobility_data/importers/gas_filling_station.py b/mobility_data/importers/gas_filling_station.py index c00fda744..40430042b 100644 --- a/mobility_data/importers/gas_filling_station.py +++ b/mobility_data/importers/gas_filling_station.py @@ -115,10 +115,10 @@ def save_to_database(objects, delete_tables=True): is_active=is_active, geometry=object.geometry, extra=object.extra, - content_type=content_type, address_zip=object.address_zip, municipality=object.municipality, ) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() diff --git a/mobility_data/importers/loading_unloading_places.py b/mobility_data/importers/loading_unloading_places.py index 2af4e4b64..b45988ba0 100644 --- a/mobility_data/importers/loading_unloading_places.py +++ b/mobility_data/importers/loading_unloading_places.py @@ -151,10 +151,10 @@ def save_to_database(objects, delete_tables=True): content_type = get_and_create_loading_and_unloading_place_content_type() for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, geometry=object.geometry, ) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.address_zip = object.address_zip diff --git a/mobility_data/importers/lounaistieto_shapefiles.py b/mobility_data/importers/lounaistieto_shapefiles.py index b99128a5e..010d7f349 100644 --- a/mobility_data/importers/lounaistieto_shapefiles.py +++ b/mobility_data/importers/lounaistieto_shapefiles.py @@ -122,8 +122,9 @@ def save_to_database(objects, config): return for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, geometry=object.geometry + extra=object.extra, geometry=object.geometry ) + mobile_unit.content_types.add(content_type) mobile_unit.municipality = object.municipality set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) diff --git a/mobility_data/importers/marinas.py b/mobility_data/importers/marinas.py index 16d3a4f19..678e51567 100644 --- a/mobility_data/importers/marinas.py +++ b/mobility_data/importers/marinas.py @@ -99,12 +99,12 @@ def import_marinas(delete=True): marinas.append(Marina(feature)) content_type = create_marina_content_type() for marina in marinas: - MobileUnit.objects.create( - content_type=content_type, + mobile_unit = MobileUnit.objects.create( geometry=marina.geometry, name=marina.name, extra=marina.extra, ) + mobile_unit.content_types.add(content_type) return len(marinas) @@ -128,4 +128,5 @@ def import_guest_marina_and_boat_parking(delete=True): elif type_name == BOAT_PARKING: content_type = create_boat_parking_content_type() - MobileUnit.objects.create(content_type=content_type, geometry=geometry) + mobile_unit = MobileUnit.objects.create(geometry=geometry) + mobile_unit.content_types.add(content_type) diff --git a/mobility_data/importers/outdoor_gym_devices.py b/mobility_data/importers/outdoor_gym_devices.py index 6d17f64b3..13bc292aa 100644 --- a/mobility_data/importers/outdoor_gym_devices.py +++ b/mobility_data/importers/outdoor_gym_devices.py @@ -37,5 +37,7 @@ def save_outdoor_gym_devices(): content_type = create_content_type() units_qs = Unit.objects.filter(services=service) for unit in units_qs: - MobileUnit.objects.create(content_type=content_type, unit_id=unit.id) + mobile_unit = MobileUnit.objects.create(unit_id=unit.id) + mobile_unit.content_types.add(content_type) + mobile_unit.save() return units_qs.count() diff --git a/mobility_data/importers/parking_machines.py b/mobility_data/importers/parking_machines.py index 777b61f54..bacfda94f 100644 --- a/mobility_data/importers/parking_machines.py +++ b/mobility_data/importers/parking_machines.py @@ -78,9 +78,9 @@ def save_to_database(objects, delete_tables=True): content_type = get_and_create_parking_machine_content_type() for object in objects: - MobileUnit.objects.create( - content_type=content_type, + mobile_unit = MobileUnit.objects.create( address=object.address, geometry=object.geometry, extra=object.extra, ) + mobile_unit.content_types.add(content_type) diff --git a/mobility_data/importers/share_car_parking_places.py b/mobility_data/importers/share_car_parking_places.py index 6765a8cb3..87e25f9d0 100644 --- a/mobility_data/importers/share_car_parking_places.py +++ b/mobility_data/importers/share_car_parking_places.py @@ -95,9 +95,8 @@ def save_to_database(objects, delete_tables=True): delete_car_share_parking_places() content_type = create_car_share_parking_place_content_type() for object in objects: - mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra - ) + mobile_unit = MobileUnit.objects.create(extra=object.extra) + mobile_unit.content_types.add(content_type) set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) mobile_unit.save() diff --git a/mobility_data/importers/utils.py b/mobility_data/importers/utils.py index 87411ffac..bfed1c31b 100644 --- a/mobility_data/importers/utils.py +++ b/mobility_data/importers/utils.py @@ -78,10 +78,10 @@ def create_mobile_unit_as_unit_reference(unit_id, content_type): serialize the data from the services_unit table in the mobile_unit endpoint. """ - MobileUnit.objects.create( + mobile_unit = MobileUnit.objects.create( unit_id=unit_id, - content_type=content_type, ) + mobile_unit.content_types.add(content_type) def get_or_create_content_type(name, description): diff --git a/mobility_data/importers/wfs.py b/mobility_data/importers/wfs.py index bcfd2a6b6..bb15af06e 100644 --- a/mobility_data/importers/wfs.py +++ b/mobility_data/importers/wfs.py @@ -47,8 +47,9 @@ def save_to_database_using_yaml_config(objects, config): return for object in objects: mobile_unit = MobileUnit.objects.create( - content_type=content_type, extra=object.extra, geometry=object.geometry + extra=object.extra, geometry=object.geometry ) + mobile_unit.content_types.add(content_type) mobile_unit.municipality = object.municipality set_translated_field(mobile_unit, "name", object.name) set_translated_field(mobile_unit, "address", object.address) From 2c550462edf7fd0d4513d49dc9832c43cb32d717 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:57:06 +0200 Subject: [PATCH 114/188] Set ordering by name --- mobility_data/models/content_type.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/models/content_type.py b/mobility_data/models/content_type.py index 375166065..9dbec462e 100644 --- a/mobility_data/models/content_type.py +++ b/mobility_data/models/content_type.py @@ -12,7 +12,7 @@ class BaseType(models.Model): class Meta: abstract = True - ordering = ["id"] + ordering = ["name"] def __str__(self): return self.name From f9f4f87098c5906e61fa961c1c93021e6d47e05f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 10:59:21 +0200 Subject: [PATCH 115/188] Add m2m field content_types, remove content_type field --- mobility_data/models/mobile_unit.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mobility_data/models/mobile_unit.py b/mobility_data/models/mobile_unit.py index d5edbe44f..0054346f6 100644 --- a/mobility_data/models/mobile_unit.py +++ b/mobility_data/models/mobile_unit.py @@ -53,9 +53,7 @@ class MobileUnit(BaseUnit): ) address_zip = models.CharField(max_length=10, null=True) - content_type = models.ForeignKey( - ContentType, on_delete=models.CASCADE, related_name="units" - ) + content_types = models.ManyToManyField(ContentType, related_name="mobile_units") unit_id = models.IntegerField( null=True, verbose_name="optional id to a unit in the servicemap, if id exist data is serialized from services_unit table", From 4349e65e8538a740a1b47fbd6cb253380c95c3a2 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 11:00:08 +0200 Subject: [PATCH 116/188] Add content_type and MobileUnit with 2 content types --- mobility_data/tests/conftest.py | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/mobility_data/tests/conftest.py b/mobility_data/tests/conftest.py index 712225228..a21da3a5f 100644 --- a/mobility_data/tests/conftest.py +++ b/mobility_data/tests/conftest.py @@ -46,11 +46,15 @@ def api_client(): @pytest.mark.django_db @pytest.fixture -def content_type(): - content_type = ContentType.objects.create( - name="Test", description="test content type" +def content_types(): + content_types = [ + ContentType.objects.create(name="Test", description="test content type") + ] + content_types.append( + ContentType.objects.create(name="Test2", description="test content type2") ) - return content_type + + return content_types @pytest.mark.django_db @@ -64,16 +68,26 @@ def group_type(): @pytest.mark.django_db @pytest.fixture -def mobile_unit(content_type): +def mobile_units(content_types): + mobile_units = [] extra = {"test_int": 4242, "test_float": 42.42, "test_string": "4242"} mobile_unit = MobileUnit.objects.create( name="Test mobileunit", description="Test description", - content_type=content_type, geometry=Point(42.42, 21.21, srid=settings.DEFAULT_SRID), extra=extra, ) - return mobile_unit + mobile_unit.content_types.add(content_types[0]) + mobile_units.append(mobile_units) + mobile_unit = MobileUnit.objects.create( + name="Test2 mobileunit", + description="Test2 description", + geometry=Point(43.43, 22.22, srid=settings.DEFAULT_SRID), + ) + mobile_unit.content_types.add(content_types[0]) + mobile_unit.content_types.add(content_types[1]) + mobile_units.append(mobile_units) + return mobile_units @pytest.mark.django_db From 93f3fb23041c478db017acfa2670ddf618676852 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 11:01:05 +0200 Subject: [PATCH 117/188] Test multiple content types --- mobility_data/tests/test_api.py | 57 ++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 26 deletions(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index a8cb72f46..d9a1e96a9 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -5,13 +5,13 @@ @pytest.mark.django_db -def test_content_type(api_client, content_type): +def test_content_type(api_client, content_types): url = reverse("mobility_data:content_types-list") response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test" - assert results["description"] == "test content type" + result = response.json()["results"][0] + assert result["name"] == "Test" + assert result["description"] == "test content type" @pytest.mark.django_db @@ -19,32 +19,37 @@ def test_group_type(api_client, group_type): url = reverse("mobility_data:group_types-list") response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "TestGroup" - assert results["description"] == "test group type" + result = response.json()["results"][0] + assert result["name"] == "TestGroup" + assert result["description"] == "test group type" @pytest.mark.django_db -def test_mobile_unit(api_client, mobile_unit, content_type): +def test_mobile_unit(api_client, mobile_units, content_types): url = reverse("mobility_data:mobile_units-list") response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test mobileunit" - assert results["description"] == "Test description" - assert results["content_type"]["id"] == str(content_type.id) - assert results["extra"]["test_string"] == "4242" - assert results["extra"]["test_int"] == 4242 - assert results["extra"]["test_float"] == 42.42 - assert results["geometry"] == Point(42.42, 21.21, srid=settings.DEFAULT_SRID) + result = response.json()["results"][1] + assert result["name"] == "Test mobileunit" + assert result["description"] == "Test description" + assert result["content_types"][0]["id"] == str(content_types[0].id) + assert result["extra"]["test_string"] == "4242" + assert result["extra"]["test_int"] == 4242 + assert result["extra"]["test_float"] == 42.42 + assert result["geometry"] == Point(42.42, 21.21, srid=settings.DEFAULT_SRID) + # Test multiple content types + result = response.json()["results"][0] + assert len(result["content_types"]) == 2 + assert result["content_types"][0]["name"] == "Test" + assert result["content_types"][1]["name"] == "Test2" url = ( reverse("mobility_data:mobile_units-list") + "?type_name=Test?extra__test_string=4242" ) response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test mobileunit" + result = response.json()["results"][1] + assert result["name"] == "Test mobileunit" # Test int value in extra field url = ( reverse("mobility_data:mobile_units-list") @@ -52,8 +57,8 @@ def test_mobile_unit(api_client, mobile_unit, content_type): ) response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test mobileunit" + result = response.json()["results"][1] + assert result["name"] == "Test mobileunit" # Test float value in extra field url = ( reverse("mobility_data:mobile_units-list") @@ -61,8 +66,8 @@ def test_mobile_unit(api_client, mobile_unit, content_type): ) response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test mobileunit" + result = response.json()["results"][1] + assert result["name"] == "Test mobileunit" @pytest.mark.django_db @@ -70,7 +75,7 @@ def test_mobile_unit_group(api_client, mobile_unit_group, group_type): url = reverse("mobility_data:mobile_unit_groups-list") response = api_client.get(url) assert response.status_code == 200 - results = response.json()["results"][0] - assert results["name"] == "Test mobileunitgroup" - assert results["description"] == "Test description" - assert results["group_type"]["id"] == str(group_type.id) + result = response.json()["results"][0] + assert result["name"] == "Test mobileunitgroup" + assert result["description"] == "Test description" + assert result["group_type"]["id"] == str(group_type.id) From dd59d0fb52cae2e94a710ab16769ddc8d8becdf5 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 11:02:55 +0200 Subject: [PATCH 118/188] Change .all()[0] to .first() --- mobility_data/tests/test_import_bicycle_stands.py | 2 +- smbackend_turku/tests/test_bike_service_stations.py | 2 +- smbackend_turku/tests/test_charging_stations.py | 4 ++-- smbackend_turku/tests/test_gas_filling_stations.py | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobility_data/tests/test_import_bicycle_stands.py b/mobility_data/tests/test_import_bicycle_stands.py index e5b9a7ae8..c58cc7396 100644 --- a/mobility_data/tests/test_import_bicycle_stands.py +++ b/mobility_data/tests/test_import_bicycle_stands.py @@ -49,7 +49,7 @@ def test_wfs_importer( import_command("import_bicycle_stands", test_mode="bicycle_stands.gml") assert MobileUnit.objects.all().count() == 3 # 0 in fixture xml. - stand_normal = MobileUnit.objects.all()[0] + stand_normal = MobileUnit.objects.first() # 182213917 in fixture xml. stand_covered_hull_lockable = MobileUnit.objects.all()[1] # 319490982 in fixture xml diff --git a/smbackend_turku/tests/test_bike_service_stations.py b/smbackend_turku/tests/test_bike_service_stations.py index e43847e8b..b8dfe7fc1 100644 --- a/smbackend_turku/tests/test_bike_service_stations.py +++ b/smbackend_turku/tests/test_bike_service_stations.py @@ -31,7 +31,7 @@ def test_bike_service_stations_import( ) assert Unit.objects.all().count() == 3 Service.objects.all().count() == 1 - service = Service.objects.all()[0] + service = Service.objects.first() assert service.name == config["service"]["name"]["fi"] assert service.name_sv == config["service"]["name"]["sv"] assert service.name_en == config["service"]["name"]["en"] diff --git a/smbackend_turku/tests/test_charging_stations.py b/smbackend_turku/tests/test_charging_stations.py index 459e2ecf9..095ec1bfe 100644 --- a/smbackend_turku/tests/test_charging_stations.py +++ b/smbackend_turku/tests/test_charging_stations.py @@ -34,7 +34,7 @@ def test_charging_stations_import( ) assert Unit.objects.all().count() == 3 Service.objects.all().count() == 1 - service = Service.objects.all()[0] + service = Service.objects.first() assert service.name == config["service"]["name"]["fi"] assert service.name_sv == config["service"]["name"]["sv"] assert service.name_en == config["service"]["name"]["en"] @@ -49,4 +49,4 @@ def test_charging_stations_import( assert aimopark.address_zip == "20100" assert aimopark.root_service_nodes == "42" assert aimopark.services.count() == 1 - assert aimopark.services.all()[0] == service + assert aimopark.services.first() == service diff --git a/smbackend_turku/tests/test_gas_filling_stations.py b/smbackend_turku/tests/test_gas_filling_stations.py index 4882818d4..afdf4b749 100644 --- a/smbackend_turku/tests/test_gas_filling_stations.py +++ b/smbackend_turku/tests/test_gas_filling_stations.py @@ -42,5 +42,5 @@ def test_gas_filling_stations_import(): assert unit.extra["operator"] == "Gasum" assert unit.service_nodes.all().count() == 1 assert unit.services.all().count() == 1 - assert unit.services.all()[0].name == config["service"]["name"]["fi"] - assert unit.service_nodes.all()[0].name == config["service_node"]["name"]["fi"] + assert unit.services.first().name == config["service"]["name"]["fi"] + assert unit.service_nodes.first().name == config["service_node"]["name"]["fi"] From 2ccc5cf6abeb05ed37317e21418ee5b550d70add Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 11:03:57 +0200 Subject: [PATCH 119/188] Change content_type field to m2m field content_types --- .../tests/test_import_accessories.py | 29 ++++++++++--------- .../test_import_bike_service_stations.py | 2 +- .../tests/test_import_charging_stations.py | 2 +- ...t_import_disabled_and_no_staff_parkings.py | 13 ++++++--- .../test_import_foli_parkandride_stops.py | 14 ++++----- mobility_data/tests/test_import_foli_stops.py | 3 +- .../tests/test_import_gas_filling_stations.py | 2 +- ...est_import_loading_and_unloading_places.py | 4 ++- .../tests/test_import_parking_machines.py | 3 +- .../tests/test_import_payment_zones.py | 6 ++-- .../tests/test_import_scooter_restrictions.py | 15 ++++++---- .../test_import_share_car_parking_places.py | 2 +- .../tests/test_import_speed_limits.py | 8 ++--- 13 files changed, 59 insertions(+), 44 deletions(-) diff --git a/mobility_data/tests/test_import_accessories.py b/mobility_data/tests/test_import_accessories.py index 36ca78400..c81b005da 100644 --- a/mobility_data/tests/test_import_accessories.py +++ b/mobility_data/tests/test_import_accessories.py @@ -30,13 +30,13 @@ def test_import_accessories( ) public_toilet_content_type = ContentType.objects.get(name="PublicToilet") - assert public_toilet_content_type public_toilet_units_qs = MobileUnit.objects.filter( - content_type=public_toilet_content_type + content_types=public_toilet_content_type ) assert public_toilet_units_qs.count() == 2 public_toilet_unit = public_toilet_units_qs[0] - assert public_toilet_unit.content_type == public_toilet_content_type + assert public_toilet_unit.content_types.all().count() == 1 + assert public_toilet_unit.content_types.first() == public_toilet_content_type extra = public_toilet_unit.extra assert extra["Kunto"] == "Ei tietoa" assert extra["Malli"] == "Testi Vessa" @@ -52,26 +52,29 @@ def test_import_accessories( assert extra["Varustelaji_koodi"] == 4022 bench_content_type = ContentType.objects.get(name="PublicBench") - assert bench_content_type - - bench_units_qs = MobileUnit.objects.filter(content_type=bench_content_type) + bench_units_qs = MobileUnit.objects.filter(content_types=bench_content_type) # Bench id 107620803 locates in Kaarina and therefore is not included. assert bench_units_qs.count() == 1 bench_unit = bench_units_qs.first() - assert bench_unit.content_type == bench_content_type + assert bench_unit.content_types.all().count() == 1 + assert bench_unit.content_types.first() == bench_content_type point = Point(23464051.217, 6706051.818, srid=DEFAULT_SOURCE_DATA_SRID) point.transform(settings.DEFAULT_SRID) bench_unit.geometry.equals_exact(point, tolerance=0.0001) + table_content_type = ContentType.objects.get(name="PublicTable") - assert table_content_type - table_units_qs = MobileUnit.objects.filter(content_type=table_content_type) + table_units_qs = MobileUnit.objects.filter(content_types=table_content_type) assert table_units_qs.count() == 2 - assert table_units_qs[0].content_type == table_content_type + assert table_units_qs[0].content_types.all().count() == 1 + assert table_units_qs[0].content_types.first() == table_content_type furniture_group_content_type = ContentType.objects.get(name="PublicFurnitureGroup") - assert furniture_group_content_type furniture_group_units_qs = MobileUnit.objects.filter( - content_type=furniture_group_content_type + content_types=furniture_group_content_type ) assert furniture_group_units_qs.count() == 2 - assert furniture_group_units_qs[0].content_type == furniture_group_content_type + assert furniture_group_units_qs[0].content_types.all().count() == 1 + assert ( + furniture_group_units_qs[0].content_types.first() + == furniture_group_content_type + ) diff --git a/mobility_data/tests/test_import_bike_service_stations.py b/mobility_data/tests/test_import_bike_service_stations.py index 46bcaeb98..307e61af2 100644 --- a/mobility_data/tests/test_import_bike_service_stations.py +++ b/mobility_data/tests/test_import_bike_service_stations.py @@ -12,7 +12,7 @@ def test_import_bike_service_stations(): "import_bike_service_stations", test_mode="bike_service_stations.geojson" ) assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 - assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 3 + assert MobileUnit.objects.filter(content_types__name=CONTENT_TYPE_NAME).count() == 3 kupittaankentta = MobileUnit.objects.get(name="Kupittaankenttä") assert kupittaankentta.name_sv == "Kuppisplan" assert kupittaankentta.name_en == "Kupittaa court" diff --git a/mobility_data/tests/test_import_charging_stations.py b/mobility_data/tests/test_import_charging_stations.py index e5ce52ac3..a6098fefc 100644 --- a/mobility_data/tests/test_import_charging_stations.py +++ b/mobility_data/tests/test_import_charging_stations.py @@ -21,7 +21,7 @@ def test_import_charging_stations( ): import_command("import_charging_stations", test_mode="charging_stations.csv") assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 - assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 3 + assert MobileUnit.objects.filter(content_types__name=CONTENT_TYPE_NAME).count() == 3 aimopark = MobileUnit.objects.get(name="Aimopark, Yliopistonkatu 29") assert aimopark.address == "Yliopistonkatu 29" assert aimopark.address_sv == "Universitetsgatan 29" diff --git a/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py b/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py index 86226e4e0..e5120173a 100644 --- a/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py +++ b/mobility_data/tests/test_import_disabled_and_no_staff_parkings.py @@ -21,8 +21,13 @@ def test_geojson_import(municipalities): turku_muni = Municipality.objects.get(name="Turku") except Municipality.DoesNotExist: assert turku_muni + kupittaan_maauimala = MobileUnit.objects.get(name="Kupittaan maauimala") - assert kupittaan_maauimala.content_type.name == DISABLED_PARKING_CONTENT_TYPE_NAME + assert kupittaan_maauimala.content_types.all().count() == 1 + assert ( + kupittaan_maauimala.content_types.first().name + == DISABLED_PARKING_CONTENT_TYPE_NAME + ) assert kupittaan_maauimala assert kupittaan_maauimala.name_sv == "Kuppis utebad" assert kupittaan_maauimala.name_en == "Kupittaa outdoor pool" @@ -35,10 +40,9 @@ def test_geojson_import(municipalities): assert kupittaan_maauimala.extra["rajoitustyyppi"]["fi"] == "Erityisalue" assert kupittaan_maauimala.extra["rajoitustyyppi"]["sv"] == "Specialområde" assert kupittaan_maauimala.extra["rajoitustyyppi"]["en"] == "Special area" - kupittaan_seikkailupuisto = MobileUnit.objects.get(name="Kupittaan seikkailupuisto") assert ( - kupittaan_seikkailupuisto.content_type.name + kupittaan_seikkailupuisto.content_types.first().name == NO_STAFF_PARKING_CONTENT_TYPE_NAME ) assert kupittaan_seikkailupuisto @@ -51,7 +55,8 @@ def test_geojson_import(municipalities): kupittaan_urheiluhalli = MobileUnit.objects.get(name="Kupittaan urheiluhalli") assert kupittaan_urheiluhalli assert ( - kupittaan_urheiluhalli.content_type.name == NO_STAFF_PARKING_CONTENT_TYPE_NAME + kupittaan_urheiluhalli.content_types.first().name + == NO_STAFF_PARKING_CONTENT_TYPE_NAME ) assert kupittaan_urheiluhalli.name_en == "Kupittaa sports hall" assert kupittaan_urheiluhalli.extra["sahkolatauspaikkoja"] == 42 diff --git a/mobility_data/tests/test_import_foli_parkandride_stops.py b/mobility_data/tests/test_import_foli_parkandride_stops.py index d07fa5907..fde5640ff 100644 --- a/mobility_data/tests/test_import_foli_parkandride_stops.py +++ b/mobility_data/tests/test_import_foli_parkandride_stops.py @@ -30,15 +30,15 @@ def test_import_foli_stops(fetch_json_mock, municipalities): bikes_stops_content_type = ContentType.objects.get( name=FOLI_PARKANDRIDE_BIKES_STOP_CONTENT_TYPE_NAME ) - - assert cars_stops_content_type - assert bikes_stops_content_type # Fixture data contains two park and ride stops for cars and bikes. - assert MobileUnit.objects.filter(content_type=cars_stops_content_type).count() == 2 - assert MobileUnit.objects.filter(content_type=bikes_stops_content_type).count() == 2 + assert MobileUnit.objects.filter(content_types=cars_stops_content_type).count() == 2 + assert ( + MobileUnit.objects.filter(content_types=bikes_stops_content_type).count() == 2 + ) # Test Föli park and ride cars stop lieto_centre = MobileUnit.objects.get(name_en="Lieto centre, K-Supermarket Lietori") - assert lieto_centre.content_type == cars_stops_content_type + assert lieto_centre.content_types.all().count() == 1 + assert lieto_centre.content_types.first() == cars_stops_content_type assert lieto_centre.name_fi == "Lieto Keskusta, K-Supermarket Lietorin piha" assert lieto_centre.name_sv == "Lundo centrum, K-Supermarket Lietori" assert lieto_centre.address_zip == "21420" @@ -52,7 +52,7 @@ def test_import_foli_stops(fetch_json_mock, municipalities): assert lieto_centre.municipality.name == "Lieto" # Test Föli park and ride bikes stop raisio_st1 = MobileUnit.objects.get(name_en="St1 Raisio") - assert raisio_st1.content_type == bikes_stops_content_type + assert raisio_st1.content_types.first() == bikes_stops_content_type assert raisio_st1.name_fi == "St1 Raisio" assert raisio_st1.name_sv == "St1 Raisio" assert raisio_st1.address_zip == "21200" diff --git a/mobility_data/tests/test_import_foli_stops.py b/mobility_data/tests/test_import_foli_stops.py index e940984e9..4ebb8f450 100644 --- a/mobility_data/tests/test_import_foli_stops.py +++ b/mobility_data/tests/test_import_foli_stops.py @@ -20,7 +20,8 @@ def test_import_foli_stops(fetch_json_mock): assert ContentType.objects.first().name == foli_stops.CONTENT_TYPE_NAME assert MobileUnit.objects.count() == 3 turun_satama = MobileUnit.objects.get(name="Turun satama (Silja)") - assert turun_satama.content_type == ContentType.objects.first() + assert turun_satama.content_types.all().count() == 1 + assert turun_satama.content_types.first() == ContentType.objects.first() assert turun_satama.extra["stop_code"] == "1" assert turun_satama.extra["wheelchair_boarding"] == 0 point_turun_satama = turun_satama.geometry diff --git a/mobility_data/tests/test_import_gas_filling_stations.py b/mobility_data/tests/test_import_gas_filling_stations.py index 276ba606d..29f226206 100644 --- a/mobility_data/tests/test_import_gas_filling_stations.py +++ b/mobility_data/tests/test_import_gas_filling_stations.py @@ -11,7 +11,7 @@ def test_importer(municipalities): import_command("import_gas_filling_stations", test_mode="gas_filling_stations.json") assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 - assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 2 + assert MobileUnit.objects.filter(content_types__name=CONTENT_TYPE_NAME).count() == 2 assert MobileUnit.objects.get(name="Raisio Kuninkoja") unit = MobileUnit.objects.get(name="Turku Satama") assert unit.address == "Tuontiväylä 42 abc 1-2" diff --git a/mobility_data/tests/test_import_loading_and_unloading_places.py b/mobility_data/tests/test_import_loading_and_unloading_places.py index 2406df705..5aeb10674 100644 --- a/mobility_data/tests/test_import_loading_and_unloading_places.py +++ b/mobility_data/tests/test_import_loading_and_unloading_places.py @@ -16,12 +16,14 @@ def test_import(municipalities): ) assert ContentType.objects.all().count() == 1 assert MobileUnit.objects.all().count() == 3 + turku_muni = None try: turku_muni = Municipality.objects.get(name="Turku") except Municipality.DoesNotExist: assert turku_muni lantinen_rantakatu = MobileUnit.objects.get(name="Läntinen Rantakatu") - assert lantinen_rantakatu.content_type.name == CONTENT_TYPE_NAME + assert lantinen_rantakatu.content_types.all().count() == 1 + assert lantinen_rantakatu.content_types.first().name == CONTENT_TYPE_NAME assert lantinen_rantakatu.name_sv == "Östra Strandgatan" assert lantinen_rantakatu.name_en == "Läntinen Rantakatu" assert lantinen_rantakatu.address_fi == "Läntinen Rantakatu 13" diff --git a/mobility_data/tests/test_import_parking_machines.py b/mobility_data/tests/test_import_parking_machines.py index 46148839a..e65c27cb7 100644 --- a/mobility_data/tests/test_import_parking_machines.py +++ b/mobility_data/tests/test_import_parking_machines.py @@ -20,7 +20,8 @@ def test_import_parking_machines(get_data_layer_mock): assert ContentType.objects.first().name == parking_machines.CONTENT_TYPE_NAME assert MobileUnit.objects.count() == 3 satamakatu = MobileUnit.objects.first() - assert satamakatu.content_type == ContentType.objects.first() + assert satamakatu.content_types.all().count() == 1 + assert satamakatu.content_types.first() == ContentType.objects.first() assert satamakatu.address == "Satamakatu 18 vp" assert satamakatu.extra["Malli"] == "CWT-C Touch" assert satamakatu.extra["Muuta"] == "16 € / 26 h" diff --git a/mobility_data/tests/test_import_payment_zones.py b/mobility_data/tests/test_import_payment_zones.py index e34a749ed..4f2535443 100644 --- a/mobility_data/tests/test_import_payment_zones.py +++ b/mobility_data/tests/test_import_payment_zones.py @@ -27,11 +27,11 @@ def test_import_payment_zones(): content_type = ContentType.objects.first() assert content_type.name == "PaymentZone" assert MobileUnit.objects.all().count() == 2 - payment_zone0 = MobileUnit.objects.all()[0] + payment_zone0 = MobileUnit.objects.first() payment_zone1 = MobileUnit.objects.all()[1] - payment_zone0.content_type == content_type - payment_zone1.content_type == content_type + payment_zone0.content_types.first() == content_type + payment_zone1.content_types.first() == content_type market_square = Point( 239760.23602773887, 6711049.638094525, srid=settings.DEFAULT_SRID ) diff --git a/mobility_data/tests/test_import_scooter_restrictions.py b/mobility_data/tests/test_import_scooter_restrictions.py index 83b53ae6f..d0393c29a 100644 --- a/mobility_data/tests/test_import_scooter_restrictions.py +++ b/mobility_data/tests/test_import_scooter_restrictions.py @@ -40,10 +40,11 @@ def test_import_scooter_restrictions(): # Test scooter parking parking_content_type = ContentType.objects.get(name="ScooterParkingArea") assert parking_content_type - parking_units_qs = MobileUnit.objects.filter(content_type=parking_content_type) + parking_units_qs = MobileUnit.objects.filter(content_types=parking_content_type) assert parking_units_qs.count() == 3 parking_unit = parking_units_qs[2] - parking_unit.content_type == parking_content_type + assert parking_unit.content_types.all().count() == 1 + parking_unit.content_types.first() == parking_content_type point = Point(239576.42, 6711050.26, srid=DEFAULT_SOURCE_DATA_SRID) parking_unit.geometry.equals_exact(point, tolerance=0.0001) import_command( @@ -54,10 +55,11 @@ def test_import_scooter_restrictions(): # Test scooter speed limits speed_limit_content_type = ContentType.objects.get(name="ScooterSpeedLimitArea") assert speed_limit_content_type - speed_limits_qs = MobileUnit.objects.filter(content_type=speed_limit_content_type) + speed_limits_qs = MobileUnit.objects.filter(content_types=speed_limit_content_type) assert speed_limits_qs.count() == 3 speed_limit_unit = MobileUnit.objects.get(id=speed_limits_qs[0].id) - assert speed_limit_unit.content_type == speed_limit_content_type + assert speed_limit_unit.content_types.all().count() == 1 + assert speed_limit_unit.content_types.first() == speed_limit_content_type market_square = Point(239755.11, 6711065.07, srid=settings.DEFAULT_SRID) turku_cathedral = Point(240377.95, 6711025.00, srid=settings.DEFAULT_SRID) # Scooter speed limit unit locates in the market square(kauppator) @@ -71,10 +73,11 @@ def test_import_scooter_restrictions(): # Test scooter no parking zones no_parking_content_type = ContentType.objects.get(name="ScooterNoParkingArea") assert no_parking_content_type - no_parking_qs = MobileUnit.objects.filter(content_type=no_parking_content_type) + no_parking_qs = MobileUnit.objects.filter(content_types=no_parking_content_type) assert no_parking_qs.count() == 3 no_parking_unit = MobileUnit.objects.get(id=no_parking_qs[0].id) - assert no_parking_unit.content_type == no_parking_content_type + assert no_parking_unit.content_types.all().count() == 1 + assert no_parking_unit.content_types.first() == no_parking_content_type aninkaisten_bridge = Point(239808.23, 6711973.03, srid=settings.DEFAULT_SRID) # no_parking_unit locates in aninkaisten bridge assert no_parking_unit.geometry.contains(aninkaisten_bridge) is True diff --git a/mobility_data/tests/test_import_share_car_parking_places.py b/mobility_data/tests/test_import_share_car_parking_places.py index 1d8cb7f7d..e3bac978d 100644 --- a/mobility_data/tests/test_import_share_car_parking_places.py +++ b/mobility_data/tests/test_import_share_car_parking_places.py @@ -12,7 +12,7 @@ def test_import_car_share_parking_places(): "import_share_car_parking_places", test_mode="share_car_parking_places.geojson" ) assert ContentType.objects.filter(name=CONTENT_TYPE_NAME).count() == 1 - assert MobileUnit.objects.filter(content_type__name=CONTENT_TYPE_NAME).count() == 3 + assert MobileUnit.objects.filter(content_types__name=CONTENT_TYPE_NAME).count() == 3 linnankatu = MobileUnit.objects.get( name="Yhteiskäyttöautojen pysäköintipaikka, Linnankatu 29" ) diff --git a/mobility_data/tests/test_import_speed_limits.py b/mobility_data/tests/test_import_speed_limits.py index 382d30b15..724da5708 100644 --- a/mobility_data/tests/test_import_speed_limits.py +++ b/mobility_data/tests/test_import_speed_limits.py @@ -29,12 +29,12 @@ def test_import_speed_limits(): assert content_type.name == "SpeedLimitZone" assert MobileUnit.objects.all().count() == 3 - zone_80 = MobileUnit.objects.all()[0] + zone_80 = MobileUnit.objects.first() zone_40 = MobileUnit.objects.all()[1] zone_20 = MobileUnit.objects.all()[2] - assert zone_80.content_type == content_type - assert zone_40.content_type == content_type - assert zone_20.content_type == content_type + assert zone_80.content_types.first() == content_type + assert zone_40.content_types.first() == content_type + assert zone_20.content_types.first() == content_type assert zone_80.extra["speed_limit"] == 80 assert zone_40.extra["speed_limit"] == 40 From 1d53e5868fe770b142f185fa11a26ba2ec3f43e7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Feb 2023 11:05:02 +0200 Subject: [PATCH 120/188] Fix typo --- smbackend_turku/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/smbackend_turku/README.md b/smbackend_turku/README.md index c8b65ec00..d1318e858 100644 --- a/smbackend_turku/README.md +++ b/smbackend_turku/README.md @@ -54,7 +54,7 @@ Importing from external data sources should always be done after importing the s smbackend_turku/importers/data/external_sources_config.yml ``` -./manage.py turku_services_import external_sorce +./manage.py turku_services_import external_sources ``` To delete all data imported from external sources: ``` From 8795f3e0f271b162cc53e913e8f14b25282ef7b4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 10 Feb 2023 08:48:13 +0200 Subject: [PATCH 121/188] Add initial source data with sv and en translations --- mobility_data/data/parking_machines.geojson | 190 ++++++++++---------- 1 file changed, 95 insertions(+), 95 deletions(-) diff --git a/mobility_data/data/parking_machines.geojson b/mobility_data/data/parking_machines.geojson index 241184713..ef83f25ab 100644 --- a/mobility_data/data/parking_machines.geojson +++ b/mobility_data/data/parking_machines.geojson @@ -3,101 +3,101 @@ "name": "Automaatit", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, "features": [ - { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, - { "type": "Feature", "properties": { "id": 2, "Osoite": "Puutarhakatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.25417454427166, 60.450266480867569 ] } }, - { "type": "Feature", "properties": { "id": 3, "Osoite": "Puutarhakatu 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.252808192099611, 60.449604877035185 ] } }, - { "type": "Feature", "properties": { "id": 4, "Osoite": "Käsityöläiskatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.257221846094581, 60.450184315580088 ] } }, - { "type": "Feature", "properties": { "id": 5, "Osoite": "Humalistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.259980071100344, 60.450177339116053 ] } }, - { "type": "Feature", "properties": { "id": 6, "Osoite": "Eerikinkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26218847141747, 60.449239333253786 ] } }, - { "type": "Feature", "properties": { "id": 7, "Osoite": "Eerikinkatu 25", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.259787917167984, 60.448419897985524 ] } }, - { "type": "Feature", "properties": { "id": 8, "Osoite": "Käsityöläiskatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.258768788015548, 60.448817764771036 ] } }, - { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, - { "type": "Feature", "properties": { "id": 10, "Osoite": "Rauhankatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.252106097225958, 60.450698403933671 ] } }, - { "type": "Feature", "properties": { "id": 11, "Osoite": "Rauhankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255241933856727, 60.451958158547647 ] } }, - { "type": "Feature", "properties": { "id": 12, "Osoite": "Rauhankatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.257154615291935, 60.452441782474324 ] } }, - { "type": "Feature", "properties": { "id": 13, "Osoite": "Humalistonkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.257216154392239, 60.451862555940885 ] } }, - { "type": "Feature", "properties": { "id": 14, "Osoite": "Läntinen pitkäkatu 24", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255051468343908, 60.452987302517734 ] } }, - { "type": "Feature", "properties": { "id": 15, "Osoite": "Läntinen pitkäkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256323063276643, 60.453437911269269 ] } }, - { "type": "Feature", "properties": { "id": 16, "Osoite": "Läntinen pitkäkatu 31", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.257994466697046, 60.454187956221354 ] } }, - { "type": "Feature", "properties": { "id": 17, "Osoite": "Läntinen pitkäkatu 23", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.261165193612193, 60.455262424830082 ] } }, - { "type": "Feature", "properties": { "id": 18, "Osoite": "Läntinen pitkäkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.264365559509113, 60.456145444039713 ] } }, - { "type": "Feature", "properties": { "id": 19, "Osoite": "Läntinen pitkäkatu 9", "Sijainti": "Viheralue", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265258802895602, 60.456688407044439 ] } }, - { "type": "Feature", "properties": { "id": 20, "Osoite": "Linja-autoasema", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.26823862990101, 60.456618323168733 ] } }, - { "type": "Feature", "properties": { "id": 21, "Osoite": "Brahenkatu 18-20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265432860869307, 60.45609461101597 ] } }, - { "type": "Feature", "properties": { "id": 22, "Osoite": "Tuureporinkatu 13", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.267508058545992, 60.455899614161197 ] } }, - { "type": "Feature", "properties": { "id": 23, "Osoite": "Tuureporinkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.265579060229619, 60.45501128227162 ] } }, - { "type": "Feature", "properties": { "id": 24, "Osoite": "Kauppiaskatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.263927120185748, 60.455138542084491 ] } }, - { "type": "Feature", "properties": { "id": 25, "Osoite": "Kauppiaskatu 17", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.265027327999253, 60.454324227152817 ] } }, - { "type": "Feature", "properties": { "id": 26, "Osoite": "Puutori", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "26.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268197772640576, 60.455031569350027 ] } }, - { "type": "Feature", "properties": { "id": 27, "Osoite": "Maariankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.269849896888495, 60.454839172502602 ] } }, - { "type": "Feature", "properties": { "id": 28, "Osoite": "Brahenkatu 10", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268764353572763, 60.453721815846308 ] } }, - { "type": "Feature", "properties": { "id": 29, "Osoite": "Brahenkatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.270232241076812, 60.452956273815545 ] } }, - { "type": "Feature", "properties": { "id": 30, "Osoite": "Yliopistonkatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.270612920328336, 60.453720163907732 ] } }, - { "type": "Feature", "properties": { "id": 31, "Osoite": "Nahkurinkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.27195361111416, 60.454879189541835 ] } }, - { "type": "Feature", "properties": { "id": 32, "Osoite": "Multavierunkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274649677455383, 60.454469280317213 ] } }, - { "type": "Feature", "properties": { "id": 33, "Osoite": "Eerikinkatu 3 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274411237269273, 60.453231364393154 ] } }, - { "type": "Feature", "properties": { "id": 34, "Osoite": "Aurakatu 22", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26318967618387, 60.452955972059186 ] } }, - { "type": "Feature", "properties": { "id": 35, "Osoite": "Maariankatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "17.12.2021", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.266164317389642, 60.453604384909475 ] } }, - { "type": "Feature", "properties": { "id": 36, "Osoite": "Puolalankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.261904591298897, 60.452405246993045 ] } }, - { "type": "Feature", "properties": { "id": 37, "Osoite": "Puolalankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.262568837946596, 60.451667570101037 ] } }, - { "type": "Feature", "properties": { "id": 38, "Osoite": "Kauppahalli", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.265780082893588, 60.449524771568896 ] } }, - { "type": "Feature", "properties": { "id": 39, "Osoite": "Linnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.273690297937019, 60.451923978676078 ] } }, - { "type": "Feature", "properties": { "id": 40, "Osoite": "Aurakatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.268066143927651, 60.44930830363591 ] } }, - { "type": "Feature", "properties": { "id": 41, "Osoite": "Kauppiaskatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.27064815615228, 60.450128994763617 ] } }, - { "type": "Feature", "properties": { "id": 42, "Osoite": "Linnankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.271717105625264, 60.451106984503213 ] } }, - { "type": "Feature", "properties": { "id": 43, "Osoite": "Borenpuisto/aukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256426088884556, 60.445362011920302 ] } }, - { "type": "Feature", "properties": { "id": 44, "Osoite": "Eerikinkatu 38", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.254031506456919, 60.446270987052962 ] } }, - { "type": "Feature", "properties": { "id": 45, "Osoite": "Eerikinkatu 40", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.252807701852902, 60.44585626467363 ] } }, - { "type": "Feature", "properties": { "id": 46, "Osoite": "Sairashuoneenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.251775173461883, 60.444872332201818 ] } }, - { "type": "Feature", "properties": { "id": 47, "Osoite": "Puistokatu 12", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.250455226870177, 60.447872484202826 ] } }, - { "type": "Feature", "properties": { "id": 48, "Osoite": "Itsenäisyydenaukio 2 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.262710182022698, 60.445569236961369 ] } }, - { "type": "Feature", "properties": { "id": 50, "Osoite": "Itäinen pitkäkatu/Olavinpuisto vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.269007832110184, 60.447885003377571 ] } }, - { "type": "Feature", "properties": { "id": 51, "Osoite": "Kaskenkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CTW-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.271248289746925, 60.447359092619095 ] } }, - { "type": "Feature", "properties": { "id": 52, "Osoite": "Hämeenkatu 28", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.272740939586971, 60.448712477636697 ] } }, - { "type": "Feature", "properties": { "id": 53, "Osoite": "Hämeenkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.274129556670403, 60.449348582880177 ] } }, - { "type": "Feature", "properties": { "id": 54, "Osoite": "Hämeenkatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.27594945421551, 60.449947345346075 ] } }, - { "type": "Feature", "properties": { "id": 55, "Osoite": "Nunnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.273923382374562, 60.450153277326237 ] } }, - { "type": "Feature", "properties": { "id": 56, "Osoite": "Uudenmaankatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279477989161659, 60.449038532856854 ] } }, - { "type": "Feature", "properties": { "id": 57, "Osoite": "Vähä Hämeenkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279883914491133, 60.449633311242088 ] } }, - { "type": "Feature", "properties": { "id": 58, "Osoite": "Hovioikeudenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280643577273363, 60.450943454869588 ] } }, - { "type": "Feature", "properties": { "id": 59, "Osoite": "Tuomiokirkontori 1-3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.277983862006817, 60.451173921939066 ] } }, - { "type": "Feature", "properties": { "id": 60, "Osoite": "Tuomiokirkonkatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.280249199106677, 60.451952316001226 ] } }, - { "type": "Feature", "properties": { "id": 61, "Osoite": "Kerttulinkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.282016000564631, 60.452352972963375 ] } }, - { "type": "Feature", "properties": { "id": 62, "Osoite": "Hämeenkatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.283591614208678, 60.452205864044352 ] } }, - { "type": "Feature", "properties": { "id": 63, "Osoite": "Rehtorinpellonkatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.287462642661801, 60.453937416413524 ] } }, - { "type": "Feature", "properties": { "id": 64, "Osoite": "Vähä Hämeenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.28764674597139, 60.452178003199926 ] } }, - { "type": "Feature", "properties": { "id": 65, "Osoite": "Lemminkäisenkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.294781520449281, 60.448205452316486 ] } }, - { "type": "Feature", "properties": { "id": 66, "Osoite": "Lemminkäisenkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.296182597025911, 60.447646306211979 ] } }, - { "type": "Feature", "properties": { "id": 67, "Osoite": "Puutarhakatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.260430472827235, 60.45216772994651 ] } }, - { "type": "Feature", "properties": { "id": 68, "Osoite": "Humalistonkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "29.1.2018", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.258675701953273, 60.450865575940362 ] } }, - { "type": "Feature", "properties": { "id": 69, "Osoite": "Puutarhakatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.26175837633734, 60.45287615088543 ] } }, - { "type": "Feature", "properties": { "id": 70, "Osoite": "Käsityöläiskatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "27.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.25604659396264, 60.450985440937544 ] } }, - { "type": "Feature", "properties": { "id": 71, "Osoite": "Yliopistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT Touch", "Asennettu": "11.10.2022", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.273213346106335, 60.454614196330851 ] } }, - { "type": "Feature", "properties": { "id": 72, "Osoite": "Itäinen Rantakatu 72", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.243079545713517, 60.437818487078545 ] } }, - { "type": "Feature", "properties": { "id": 73, "Osoite": "Kasarminkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280352370046622, 60.457669207319114 ] } }, - { "type": "Feature", "properties": { "id": 75, "Osoite": "Linnankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1" }, "geometry": { "type": "Point", "coordinates": [ 22.264872376936172, 60.448691040677524 ] } }, - { "type": "Feature", "properties": { "id": 77, "Osoite": "Piispankatu 10", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.279733274298824, 60.455020619216043 ] } }, - { "type": "Feature", "properties": { "id": 78, "Osoite": "Piispankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale§", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.277760964164081, 60.453216079342297 ] } }, - { "type": "Feature", "properties": { "id": 79, "Osoite": "Porthaninkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.28073811293207, 60.453918201523102 ] } }, - { "type": "Feature", "properties": { "id": 84, "Osoite": "Tehtaankatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.280930555107421, 60.45626865766279 ] } }, - { "type": "Feature", "properties": { "id": 87, "Osoite": "Ursiininkatu 9", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.256546650434711, 60.448655233433726 ] } }, - { "type": "Feature", "properties": { "id": 88, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.29275674703927, 60.443803487057082 ] } }, - { "type": "Feature", "properties": { "id": 89, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.294113833178297, 60.442723807754163 ] } }, - { "type": "Feature", "properties": { "id": 90, "Osoite": "Tahkonaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.294175006719268, 60.446804509763439 ] } }, - { "type": "Feature", "properties": { "id": 91, "Osoite": "Teollisuuskatu 16", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT Touch", "Asennettu": "11.10.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3" }, "geometry": { "type": "Point", "coordinates": [ 22.301980689265612, 60.449590340262404 ] } }, - { "type": "Feature", "properties": { "id": 300, "Osoite": "Kunnallissairaalantie 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.275377662478608, 60.440746141486549 ] } }, - { "type": "Feature", "properties": { "id": 301, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.274444434528675, 60.438998327974652 ] } }, - { "type": "Feature", "properties": { "id": 302, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/h ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala" }, "geometry": { "type": "Point", "coordinates": [ 22.274072124365258, 60.439409437701471 ] } }, - { "type": "Feature", "properties": { "id": 401, "Osoite": "4. linja ", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "13 €/26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.2227213543412, 60.435316415968622 ] } }, - { "type": "Feature", "properties": { "id": 403, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.221447167201752, 60.435584779574739 ] } }, - { "type": "Feature", "properties": { "id": 404, "Osoite": "3. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.221389745850257, 60.435173290652273 ] } }, - { "type": "Feature", "properties": { "id": 405, "Osoite": "Linnankatu 91", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.219860237204227, 60.435680281344297 ] } }, - { "type": "Feature", "properties": { "id": 406, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.224974204395551, 60.433923001505228 ] } }, - { "type": "Feature", "properties": { "id": 407, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225946466932712, 60.434033761861251 ] } }, - { "type": "Feature", "properties": { "id": 408, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.224755030627708, 60.434824489085884 ] } }, - { "type": "Feature", "properties": { "id": 409, "Osoite": "Linnankatu 87", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225127681727219, 60.435830090490398 ] } }, - { "type": "Feature", "properties": { "id": 413, "Osoite": "Satamakatu 18", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.225972071802026, 60.436572492254285 ] } }, - { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } + { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Trädgårdsgatan 15", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.7.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Puutarhakatu 15", "Installed": "3.7.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, + { "type": "Feature", "properties": { "id": 2, "Osoite": "Puutarhakatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Trädgårdsgatan 19", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.7.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Puutarhakatu 19", "Installed": "3.7.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.25417454427166, 60.450266480867569 ] } }, + { "type": "Feature", "properties": { "id": 3, "Osoite": "Puutarhakatu 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Trädgårdsgatan 20", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.7.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Puutarhakatu 20", "Installed": "3.7.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.252808192099611, 60.449604877035185 ] } }, + { "type": "Feature", "properties": { "id": 4, "Osoite": "Käsityöläiskatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Hantverkaregatan 7", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "12.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Käsityöläiskatu 7", "Installed": "12.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.257221846094581, 60.450184315580088 ] } }, + { "type": "Feature", "properties": { "id": 5, "Osoite": "Humalistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Humlegårdsgatan 5", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "12.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Humalistonkatu 5", "Installed": "12.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.259980071100344, 60.450177339116053 ] } }, + { "type": "Feature", "properties": { "id": 6, "Osoite": "Eerikinkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "12.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Eriksgatan 21", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "12.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Eerikinkatu 21", "Installed": "12.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.26218847141747, 60.449239333253786 ] } }, + { "type": "Feature", "properties": { "id": 7, "Osoite": "Eerikinkatu 25", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Eriksgatan 25", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Eerikinkatu 25", "Installed": "17.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.259787917167984, 60.448419897985524 ] } }, + { "type": "Feature", "properties": { "id": 8, "Osoite": "Käsityöläiskatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Hantverkaregatan 4", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "13.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Käsityöläiskatu 4", "Installed": "13.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.258768788015548, 60.448817764771036 ] } }, + { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Ursinsgatan 4", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "13.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Ursiininkatu 4", "Installed": "13.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, + { "type": "Feature", "properties": { "id": 10, "Osoite": "Rauhankatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Fredsgatan 14", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "13.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Rauhankatu 14", "Installed": "13.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.252106097225958, 60.450698403933671 ] } }, + { "type": "Feature", "properties": { "id": 11, "Osoite": "Rauhankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Fredsgatan 5", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "13.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Rauhankatu 5", "Installed": "13.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.255241933856727, 60.451958158547647 ] } }, + { "type": "Feature", "properties": { "id": 12, "Osoite": "Rauhankatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Fredsgatan 4", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Rauhankatu 4", "Installed": "17.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.257154615291935, 60.452441782474324 ] } }, + { "type": "Feature", "properties": { "id": 13, "Osoite": "Humalistonkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Humlegårdsgatan 14", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Humalistonkatu 14", "Installed": "17.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.257216154392239, 60.451862555940885 ] } }, + { "type": "Feature", "properties": { "id": 14, "Osoite": "Läntinen pitkäkatu 24", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 24", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 24", "Installed": "17.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.255051468343908, 60.452987302517734 ] } }, + { "type": "Feature", "properties": { "id": 15, "Osoite": "Läntinen pitkäkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 22", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 22", "Installed": "17.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.256323063276643, 60.453437911269269 ] } }, + { "type": "Feature", "properties": { "id": 16, "Osoite": "Läntinen pitkäkatu 31", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 31", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "18.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 31", "Installed": "18.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.257994466697046, 60.454187956221354 ] } }, + { "type": "Feature", "properties": { "id": 17, "Osoite": "Läntinen pitkäkatu 23", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 23", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "18.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 23", "Installed": "18.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.261165193612193, 60.455262424830082 ] } }, + { "type": "Feature", "properties": { "id": 18, "Osoite": "Läntinen pitkäkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 6", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 6", "Installed": "25.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.264365559509113, 60.456145444039713 ] } }, + { "type": "Feature", "properties": { "id": 19, "Osoite": "Läntinen pitkäkatu 9", "Sijainti": "Viheralue", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Västerlånggatan 9", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Läntinen pitkäkatu 9", "Installed": "25.8.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.265258802895602, 60.456688407044439 ] } }, + { "type": "Feature", "properties": { "id": 20, "Osoite": "Linja-autoasema", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Busstationen", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Bus Station", "Installed": "25.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.26823862990101, 60.456618323168733 ] } }, + { "type": "Feature", "properties": { "id": 21, "Osoite": "Brahenkatu 18-20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Brahegatan 18-20", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Brahenkatu 18-20", "Installed": "25.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.265432860869307, 60.45609461101597 ] } }, + { "type": "Feature", "properties": { "id": 22, "Osoite": "Tuureporinkatu 13", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tureborgsgatan 13", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Tuureporinkatu 13", "Installed": "25.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.267508058545992, 60.455899614161197 ] } }, + { "type": "Feature", "properties": { "id": 23, "Osoite": "Tuureporinkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "25.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tureborgsgatan 16", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "25.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Tuureporinkatu 16", "Installed": "25.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.265579060229619, 60.45501128227162 ] } }, + { "type": "Feature", "properties": { "id": 24, "Osoite": "Kauppiaskatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Köpmansgatan 19", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "27.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Kauppiaskatu 19", "Installed": "27.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.263927120185748, 60.455138542084491 ] } }, + { "type": "Feature", "properties": { "id": 25, "Osoite": "Kauppiaskatu 17", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Köpmansgatan 17", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "27.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Kauppiaskatu 17", "Installed": "27.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.265027327999253, 60.454324227152817 ] } }, + { "type": "Feature", "properties": { "id": 26, "Osoite": "Puutori", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "26.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Trätorget", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "26.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Puutori", "Installed": "26.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.268197772640576, 60.455031569350027 ] } }, + { "type": "Feature", "properties": { "id": 27, "Osoite": "Maariankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Mariegatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "27.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Maariankatu 2", "Installed": "27.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.269849896888495, 60.454839172502602 ] } }, + { "type": "Feature", "properties": { "id": 28, "Osoite": "Brahenkatu 10", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Brahegatan 10", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "27.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Brahenkatu 10", "Installed": "27.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.268764353572763, 60.453721815846308 ] } }, + { "type": "Feature", "properties": { "id": 29, "Osoite": "Brahenkatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "27.8.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Brahegatan 7", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "27.8.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Brahenkatu 7", "Installed": "27.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.270232241076812, 60.452956273815545 ] } }, + { "type": "Feature", "properties": { "id": 30, "Osoite": "Yliopistonkatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Universitetsgatan 11", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "9.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Yliopistonkatu 11", "Installed": "9.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.270612920328336, 60.453720163907732 ] } }, + { "type": "Feature", "properties": { "id": 31, "Osoite": "Nahkurinkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Logarvaregatan 8", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Nahkurinkatu 8", "Installed": "10.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.27195361111416, 60.454879189541835 ] } }, + { "type": "Feature", "properties": { "id": 32, "Osoite": "Multavierunkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Multavierugatan 1", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Multavierunkatu 1", "Installed": "10.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.274649677455383, 60.454469280317213 ] } }, + { "type": "Feature", "properties": { "id": 33, "Osoite": "Eerikinkatu 3 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Eriksgatan 3 me", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "9.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Eerikinkatu 3 vp", "Installed": "9.9.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.274411237269273, 60.453231364393154 ] } }, + { "type": "Feature", "properties": { "id": 34, "Osoite": "Aurakatu 22", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Auragatan 22", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Aurakatu 22", "Installed": "10.9.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.26318967618387, 60.452955972059186 ] } }, + { "type": "Feature", "properties": { "id": 35, "Osoite": "Maariankatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.12.2021", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Mariegatan 8", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "17.12.2021", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Maariankatu 8", "Installed": "17.12.2021", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.266164317389642, 60.453604384909475 ] } }, + { "type": "Feature", "properties": { "id": 36, "Osoite": "Puolalankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Puolalagatan 5", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "9.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Puolalankatu 5", "Installed": "9.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.261904591298897, 60.452405246993045 ] } }, + { "type": "Feature", "properties": { "id": 37, "Osoite": "Puolalankatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Puolalagatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Puolalankatu 2", "Installed": "10.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.262568837946596, 60.451667570101037 ] } }, + { "type": "Feature", "properties": { "id": 38, "Osoite": "Kauppahalli", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Saluhallet", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "11.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Market Hall", "Installed": "11.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.265780082893588, 60.449524771568896 ] } }, + { "type": "Feature", "properties": { "id": 39, "Osoite": "Linnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "11.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Slotsgatan 1", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "11.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Linnankatu 1", "Installed": "11.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.273690297937019, 60.451923978676078 ] } }, + { "type": "Feature", "properties": { "id": 40, "Osoite": "Aurakatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Auragatan 2", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Aurakatu 2", "Installed": "10.9.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.268066143927651, 60.44930830363591 ] } }, + { "type": "Feature", "properties": { "id": 41, "Osoite": "Kauppiaskatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Köpmansgatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Kauppiaskatu 2", "Installed": "10.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.27064815615228, 60.450128994763617 ] } }, + { "type": "Feature", "properties": { "id": 42, "Osoite": "Linnankatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "10.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Slottsgatan 5", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "10.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Linnankatu 5", "Installed": "10.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.271717105625264, 60.451106984503213 ] } }, + { "type": "Feature", "properties": { "id": 43, "Osoite": "Borenpuisto/aukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Boreparken/plan", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "15.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Borenpuisto/aukio", "Installed": "15.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.256426088884556, 60.445362011920302 ] } }, + { "type": "Feature", "properties": { "id": 44, "Osoite": "Eerikinkatu 38", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Eriksgatan 38", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "15.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Eerikinkatu 38", "Installed": "15.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.254031506456919, 60.446270987052962 ] } }, + { "type": "Feature", "properties": { "id": 45, "Osoite": "Eerikinkatu 40", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Eriksgatan 40", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "15.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Eerikinkatu 40", "Installed": "15.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.252807701852902, 60.44585626467363 ] } }, + { "type": "Feature", "properties": { "id": 46, "Osoite": "Sairashuoneenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Lasarattsgatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "14.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Sairashuoneenkatu 2", "Installed": "14.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.251775173461883, 60.444872332201818 ] } }, + { "type": "Feature", "properties": { "id": 47, "Osoite": "Puistokatu 12", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Allegatan 12", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "14.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Puistokatu 12", "Installed": "14.9.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.250455226870177, 60.447872484202826 ] } }, + { "type": "Feature", "properties": { "id": 48, "Osoite": "Itsenäisyydenaukio 2 vp", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Självständighetsplan 2 me", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "1.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Itsenäisyydenaukio 2 vp", "Installed": "1.12.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.262710182022698, 60.445569236961369 ] } }, + { "type": "Feature", "properties": { "id": 50, "Osoite": "Itäinen pitkäkatu/Olavinpuisto vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Östra Strandsgatan/Olafsparken", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "15.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Itäinen pitkäkatu/Olavinpuisto vp", "Installed": "15.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.269007832110184, 60.447885003377571 ] } }, + { "type": "Feature", "properties": { "id": 51, "Osoite": "Kaskenkatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Kaskisgatan 1", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "2.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Kaskenkatu 1", "Installed": "2.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.271248289746925, 60.447359092619095 ] } }, + { "type": "Feature", "properties": { "id": 52, "Osoite": "Hämeenkatu 28", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tavastgatan 28", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "1.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Hämeenkatu 28", "Installed": "1.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.272740939586971, 60.448712477636697 ] } }, + { "type": "Feature", "properties": { "id": 53, "Osoite": "Hämeenkatu 21", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tavastgatan 21", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "14.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Hämeenkatu 21", "Installed": "14.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.274129556670403, 60.449348582880177 ] } }, + { "type": "Feature", "properties": { "id": 54, "Osoite": "Hämeenkatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tavastgatan 19", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "14.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Hämeenkatu 19", "Installed": "14.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.27594945421551, 60.449947345346075 ] } }, + { "type": "Feature", "properties": { "id": 55, "Osoite": "Nunnankatu 1", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Nunnegatan 1", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "2.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Nunnankatu 1", "Installed": "2.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.273923382374562, 60.450153277326237 ] } }, + { "type": "Feature", "properties": { "id": 56, "Osoite": "Uudenmaankatu 7", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "1.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Nylandsgatan 7", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "1.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Uudenmaankatu 7", "Installed": "1.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.279477989161659, 60.449038532856854 ] } }, + { "type": "Feature", "properties": { "id": 57, "Osoite": "Vähä Hämeenkatu 16", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Lilla Tavastgatan 16", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Vähä Hämeenkatu 16", "Installed": "3.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.279883914491133, 60.449633311242088 ] } }, + { "type": "Feature", "properties": { "id": 58, "Osoite": "Hovioikeudenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Hofrättsgatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "14.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Hovioikeudenkatu 2", "Installed": "14.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.280643577273363, 60.450943454869588 ] } }, + { "type": "Feature", "properties": { "id": 59, "Osoite": "Tuomiokirkontori 1-3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Domkyrkotorget 1-3", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Tuomiokirkontori 1-3", "Installed": "3.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.277983862006817, 60.451173921939066 ] } }, + { "type": "Feature", "properties": { "id": 60, "Osoite": "Tuomiokirkonkatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Domkyrkogatan 3", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "10.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Tuomiokirkonkatu 3", "Installed": "10.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.280249199106677, 60.451952316001226 ] } }, + { "type": "Feature", "properties": { "id": 61, "Osoite": "Kerttulinkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Gertrudsgatan 6", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "7.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Kerttulinkatu 6", "Installed": "7.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.282016000564631, 60.452352972963375 ] } }, + { "type": "Feature", "properties": { "id": 62, "Osoite": "Hämeenkatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Tavastgatan 6-8", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "9.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Hämeenkatu 6-8", "Installed": "9.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.283591614208678, 60.452205864044352 ] } }, + { "type": "Feature", "properties": { "id": 63, "Osoite": "Rehtorinpellonkatu 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Rektorsåkersgatan 2", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "9.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Rehtorinpellonkatu 2", "Installed": "9.12.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.287462642661801, 60.453937416413524 ] } }, + { "type": "Feature", "properties": { "id": 64, "Osoite": "Vähä Hämeenkatu 2", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Lilla Tavastgatan 2", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "9.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Vähä Hämeenkatu 2", "Installed": "9.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.28764674597139, 60.452178003199926 ] } }, + { "type": "Feature", "properties": { "id": 65, "Osoite": "Lemminkäisenkatu 14", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "9.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Lemminkäinengatan 14", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "9.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Lemminkäisenkatu 14", "Installed": "9.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.294781520449281, 60.448205452316486 ] } }, + { "type": "Feature", "properties": { "id": 66, "Osoite": "Lemminkäisenkatu 22", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "10.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Lemminkäinengatan 22", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "10.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Lemminkäisenkatu 22", "Installed": "10.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.296182597025911, 60.447646306211979 ] } }, + { "type": "Feature", "properties": { "id": 67, "Osoite": "Puutarhakatu 6-8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Trädgårdsgatan 6-8", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "6.2.2019", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Puutarhakatu 6-8", "Installed": "6.2.2019", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.260430472827235, 60.45216772994651 ] } }, + { "type": "Feature", "properties": { "id": 68, "Osoite": "Humalistonkatu 8", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "29.1.2018", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Humlegårdsgatan 8", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "29.1.2018", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Humalistonkatu 8", "Installed": "29.1.2018", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.258675701953273, 60.450865575940362 ] } }, + { "type": "Feature", "properties": { "id": 69, "Osoite": "Puutarhakatu 3", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "6.2.2019", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Trädgårdsgatan 3", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "6.2.2019", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Puutarhakatu 3", "Installed": "6.2.2019", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.26175837633734, 60.45287615088543 ] } }, + { "type": "Feature", "properties": { "id": 70, "Osoite": "Käsityöläiskatu 11", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C", "Asennettu": "27.2.2019", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "7\"", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Hantverkaregatan 11", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C", "Installerad": "27.2.2019", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "7\"", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Käsityöläiskatu 11", "Installed": "27.2.2019", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "7\"", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.25604659396264, 60.450985440937544 ] } }, + { "type": "Feature", "properties": { "id": 71, "Osoite": "Yliopistonkatu 5", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Universitetsgatan 5", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "11.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Yliopistonkatu 5", "Installed": "11.10.2022", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.273213346106335, 60.454614196330851 ] } }, + { "type": "Feature", "properties": { "id": 72, "Osoite": "Itäinen Rantakatu 72", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Östra Strandgatan 72", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "8.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Itäinen Rantakatu 72", "Installed": "8.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.243079545713517, 60.437818487078545 ] } }, + { "type": "Feature", "properties": { "id": 73, "Osoite": "Kasarminkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Kaserngatan 4", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "2.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Kasarminkatu 4", "Installed": "2.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.280352370046622, 60.457669207319114 ] } }, + { "type": "Feature", "properties": { "id": 75, "Osoite": "Linnankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 3.6, "Max.aika": null, "Maksuvyöhyke": "1", "Adress": "Slottsgatan 19", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "19.12.2022", "Ström": "Solcell", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 3.6, "Zon": "1", "Address": "Linnankatu 19", "Installed": "19.12.2022", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "1", "Tariff/h": 3.6 }, "geometry": { "type": "Point", "coordinates": [ 22.264872376936172, 60.448691040677524 ] } }, + { "type": "Feature", "properties": { "id": 77, "Osoite": "Piispankatu 10", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "2.9.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Biskopsgatan 10", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "2.9.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Piispankatu 10", "Installed": "2.9.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.279733274298824, 60.455020619216043 ] } }, + { "type": "Feature", "properties": { "id": 78, "Osoite": "Piispankatu 19", "Sijainti": "Katuosa", "Valmistaja": "Cale§", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Biskopsgatan 19", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Piispankatu 19", "Installed": "3.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.277760964164081, 60.453216079342297 ] } }, + { "type": "Feature", "properties": { "id": 79, "Osoite": "Porthaninkatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Porthansgatan 6", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "14.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Porthaninkatu 6", "Installed": "14.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.28073811293207, 60.453918201523102 ] } }, + { "type": "Feature", "properties": { "id": 84, "Osoite": "Tehtaankatu 6", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.9.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Fabriksgatan 6", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.9.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Tehtaankatu 6", "Installed": "3.9.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.280930555107421, 60.45626865766279 ] } }, + { "type": "Feature", "properties": { "id": 87, "Osoite": "Ursiininkatu 9", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "19.12.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Ursinsgatan 9", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "19.12.2022", "Ström": "Solcell", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Ursiininkatu 9", "Installed": "19.12.2022", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.256546650434711, 60.448655233433726 ] } }, + { "type": "Feature", "properties": { "id": 88, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Blombergsplan", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "16.12.2020", "Ström": "Solcell", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Blomberginaukio", "Installed": "16.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.29275674703927, 60.443803487057082 ] } }, + { "type": "Feature", "properties": { "id": 89, "Osoite": "Blomberginaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Blombergsplan", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "16.12.2020", "Ström": "Solcell", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Blomberginaukio", "Installed": "16.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.294113833178297, 60.442723807754163 ] } }, + { "type": "Feature", "properties": { "id": 90, "Osoite": "Tahkonaukio", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "16.12.2020", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Tahkoplan", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "16.12.2020", "Ström": "Solcell", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Tahkonaukio", "Installed": "16.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.294175006719268, 60.446804509763439 ] } }, + { "type": "Feature", "properties": { "id": 91, "Osoite": "Teollisuuskatu 16", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Aurinkopaneeli", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 0.6, "Max.aika": null, "Maksuvyöhyke": "3", "Adress": "Industrigatan 16", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "11.10.2022", "Ström": "Solcell", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 0.6, "Zon": "3", "Address": "Teollisuuskatu 16", "Installed": "11.10.2022", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Solar panel", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "3", "Tariff/h": 0.6 }, "geometry": { "type": "Point", "coordinates": [ 22.301980689265612, 60.449590340262404 ] } }, + { "type": "Feature", "properties": { "id": 300, "Osoite": "Kunnallissairaalantie 20", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/t ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala", "Adress": "Kommunalsjukhusvägen 20", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "8.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tillägg": "Taxa 0,5 €/t första 8t, 0,2 €/t efter 8t", "Ägare": "Åbo stad", "Taxa/t": 0.5, "Zon": "Sjukhus", "Address": "Kunnallissairaalantie 20", "Installed": "8.12.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "Tariff 0,5 €/h first 8h, 0,2 €/h time over 8h", "Owner": "City of Turku", "Zone": "Hospital", "Tariff/h": 0.5 }, "geometry": { "type": "Point", "coordinates": [ 22.275377662478608, 60.440746141486549 ] } }, + { "type": "Feature", "properties": { "id": 301, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/t ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala", "Adress": "Luolavuorivägen 2", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "8.12.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tillägg": "Taxa 0,5 €/t första 8t, 0,2 €/t efter 8t", "Ägare": "Åbo stad", "Taxa/t": 0.5, "Zon": "Sjukhus", "Address": "Luolavuorentie 2", "Installed": "8.12.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "Tariff 0,5 €/h first 8h, 0,2 €/h time over 8h", "Owner": "City of Turku", "Zone": "Hospital", "Tariff/h": 0.5 }, "geometry": { "type": "Point", "coordinates": [ 22.274444434528675, 60.438998327974652 ] } }, + { "type": "Feature", "properties": { "id": 302, "Osoite": "Luolavuorentie 2", "Sijainti": "Viherosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "8.12.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "Taksa 0,5 €/t ensimmäiset 8t, 0,2 €/t aika yli 8t", "Omistaja": "Turun kaupunki", "Taksa/h": 0.5, "Max.aika": null, "Maksuvyöhyke": "Sairaala", "Adress": "Luolavuorivägen 2", "Tillverkare": "Cale", "Plats": "Grönomrode", "Modell": "CWT-C Touch", "Installerad": "8.12.2020", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tillägg": "Taxa 0,5 €/t första 8t, 0,2 €/t efter 8t", "Ägare": "Åbo stad", "Taxa/t": 0.5, "Zon": "Sjukhus", "Address": "Luolavuorentie 2", "Installed": "8.12.2020", "Location": "Greenery", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "Tariff 0,5 €/h first 8h, 0,2 €/h time over 8h", "Owner": "City of Turku", "Zone": "Hospital", "Tariff/h": 0.5 }, "geometry": { "type": "Point", "coordinates": [ 22.274072124365258, 60.439409437701471 ] } }, + { "type": "Feature", "properties": { "id": 401, "Osoite": "4. linja ", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "4. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "18.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "4. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "18.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.2227213543412, 60.435316415968622 ] } }, + { "type": "Feature", "properties": { "id": 403, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "18.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "4. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "18.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "4. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "18.10.2022", "Power": "Mains", "Payment method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.221447167201752, 60.435584779574739 ] } }, + { "type": "Feature", "properties": { "id": 404, "Osoite": "3. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "3. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "11.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "3. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "11.10.2022", "Power": "Mains", "Payment method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.221389745850257, 60.435173290652273 ] } }, + { "type": "Feature", "properties": { "id": 405, "Osoite": "Linnankatu 91", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "Slottsgatan 91", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "7.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "Linnankatu 91", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "7.10.2022", "Power": "Mains", "Payment method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.219860237204227, 60.435680281344297 ] } }, + { "type": "Feature", "properties": { "id": 406, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "2. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "7.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "2. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "7.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3, "Max.time": null }, "geometry": { "type": "Point", "coordinates": [ 22.224974204395551, 60.433923001505228 ] } }, + { "type": "Feature", "properties": { "id": 407, "Osoite": "2. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "11.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "2. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "11.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "2. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "11.10.2022", "Power": "Mains", "Payment method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.225946466932712, 60.434033761861251 ] } }, + { "type": "Feature", "properties": { "id": 408, "Osoite": "4. linja", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "17.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "4. linjen", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "17.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "4. linja", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "17.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.224755030627708, 60.434824489085884 ] } }, + { "type": "Feature", "properties": { "id": 409, "Osoite": "Linnankatu 87", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "7.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "Slottsgatan 87", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "7.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "Linnankatu 87", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "7.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.225127681727219, 60.435830090490398 ] } }, + { "type": "Feature", "properties": { "id": 413, "Osoite": "Satamakatu 18", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "14.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "Hamngatan 18", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "14.10.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "Satamakatu 18", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "14.10.2022", "Power": "Mains", "Payment method": "Card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.225972071802026, 60.436572492254285 ] } }, + { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "Hamngatan 18 me", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "15.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "Satamakatu 18 vp", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "15.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } ] } \ No newline at end of file From fb1385c41a4266b7f42a797ded7dd37146e84299 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 09:40:41 +0200 Subject: [PATCH 122/188] Add multilingual fixture data --- mobility_data/tests/data/parking_machines.geojson | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobility_data/tests/data/parking_machines.geojson b/mobility_data/tests/data/parking_machines.geojson index bb26ac30f..afa999b1c 100644 --- a/mobility_data/tests/data/parking_machines.geojson +++ b/mobility_data/tests/data/parking_machines.geojson @@ -3,9 +3,9 @@ "name": "Automaatit", "crs": { "type": "name", "properties": { "name": "urn:ogc:def:crs:OGC:1.3:CRS84" } }, "features": [ - { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, - { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2" }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, - { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } + { "type": "Feature", "properties": { "id": 1, "Osoite": "Puutarhakatu 15", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "3.7.2020", "Virta": "Verkko", "Maksutapa": "Kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Trädgårdsgatan 15", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "3.7.2022", "Ström": "Nät", "Betalningssätt": "Kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Puutarhakatu 15", "Installed": "3.7.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.255769473435137, 60.450812192929355 ] } }, + { "type": "Feature", "properties": { "id": 9, "Osoite": "Ursiininkatu 4", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "13.8.2020", "Virta": "Verkko", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": null, "Omistaja": "Turun kaupunki", "Taksa/h": 1.8, "Max.aika": null, "Maksuvyöhyke": "2", "Adress": "Ursinsgatan 4", "Tillverkare": "Cale", "Plats": "Gata", "Modell": "CWT-C Touch", "Installerad": "13.8.2020", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Ägare": "Åbo stad", "Taxa/t": 1.8, "Zon": "2", "Address": "Ursiininkatu 4", "Installed": "13.8.2020", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Power": "Mains", "Payment Method": "Coin, card, contactless", "Screen": "9\", touch screen", "Owner": "City of Turku", "Zone": "2", "Tariff/h": 1.8 }, "geometry": { "type": "Point", "coordinates": [ 22.258686318669831, 60.446820115358349 ] } }, + { "type": "Feature", "properties": { "id": 415, "Osoite": "Satamakatu 18 vp", "Sijainti": "Katuosa", "Valmistaja": "Cale", "Malli": "CWT-C Touch", "Asennettu": "15.10.2022", "Virta": "Verkkovirta", "Maksutapa": "Kolikko, kortti, lähimaksu", "Näyttö": "9\", kosketus", "Muuta": "16 € / 26 h", "Omistaja": "Turun kaupunki", "Taksa/h": 1.3, "Max.aika": null, "Adress": "Hamngatan 18 me", "Plats": "Gata", "Tillverkare": "Cale", "Modell": "CWT-C Touch", "Installerad": "15.10.2022", "Ström": "Nät", "Betalningssätt": "Mynt, kort, kontaktlös", "Skärm": "9\", pekskärm", "Tilläg": "16 € / 26 h", "Ägare": "Åbo stad", "Zon": "Hamn", "Taxa/t": 1.3, "Maksuvyöhyke": "Satama", "Address": "Satamakatu 18 vp", "Location": "On-street", "Manufacturer": "Cale", "Model": "CWT-C Touch", "Installed": "15.10.2022", "Power": "Mains", "Payment method": "Coin, card, contactless", "Screen": "9\", touch screen", "Additional info": "16 € / 26 h", "Owner": "City of Turku", "Zone": "Harbour", "Tariff/h": 1.3 }, "geometry": { "type": "Point", "coordinates": [ 22.227764721514966, 60.436782262744913 ] } } ] } \ No newline at end of file From ea5ecf0f55effd627b55d1e7f546830f301c8dae Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 10:13:31 +0200 Subject: [PATCH 123/188] Add support for multilingual data, remove GDAL and use json data as it --- mobility_data/importers/parking_machines.py | 117 +++++++++++++++----- 1 file changed, 91 insertions(+), 26 deletions(-) diff --git a/mobility_data/importers/parking_machines.py b/mobility_data/importers/parking_machines.py index bacfda94f..eaca1c259 100644 --- a/mobility_data/importers/parking_machines.py +++ b/mobility_data/importers/parking_machines.py @@ -1,7 +1,6 @@ from django import db from django.conf import settings -from django.contrib.gis.gdal import DataSource as GDALDataSource -from django.contrib.gis.geos import GEOSGeometry +from django.contrib.gis.geos import Point from mobility_data.models import MobileUnit @@ -11,62 +10,128 @@ get_file_name_from_data_source, get_or_create_content_type, get_root_dir, + set_translated_field, ) SOURCE_DATA_SRID = 4326 GEOJSON_FILENAME = "parking_machines.geojson" CONTENT_TYPE_NAME = "ParkingMachine" +LANGUAGES = ["fi", "sv", "en"] class ParkingMachine: + extra_field_mappings = { - "Sijainti": {"type": FieldTypes.STRING}, - "Valmistaja": {"type": FieldTypes.STRING}, - "Malli": {"type": FieldTypes.STRING}, - "Asennettu": {"type": FieldTypes.STRING}, - "Virta": {"type": FieldTypes.STRING}, - "Maksutapa": {"type": FieldTypes.STRING}, - "Näyttö": {"type": FieldTypes.STRING}, + "Sijainti": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Sijainti", + "sv": "Plats", + "en": "Location", + }, + "Virta": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Virta", + "sv": "Ström", + "en": "Power", + }, + "Maksutapa": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Maksutapa", + "sv": "Betalningssätt", + # The source data contains Payment method with method starting with + # uppercase M and lowercase m. + "en": ["Payment method", "Payment Method"], + }, + "Näyttö": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Näyttö", + "sv": "Skärm", + "en": "Screen", + }, + "Omistaja": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Omistaja", + "sv": "Ägare", + "en": "Owner", + }, + "Maksuvyöhyke": { + "type": FieldTypes.MULTILANG_STRING, + "fi": "Maksuvyöhyke", + "sv": "Zon", + "en": "Zone", + }, "Muuta": {"type": FieldTypes.STRING}, - "Omistaja": {"type": FieldTypes.STRING}, "Taksa/h": {"type": FieldTypes.FLOAT}, "Max.aika": {"type": FieldTypes.FLOAT}, - "Maksuvyöhyke": {"type": FieldTypes.STRING}, + "Malli": {"type": FieldTypes.STRING}, + "Asennettu": {"type": FieldTypes.STRING}, + "Valmistaja": { + "type": FieldTypes.STRING, + }, } def __init__(self, feature): + properties = feature["properties"] + geometry = feature["geometry"] self.extra = {} - self.address = feature["Osoite"].as_string() - self.geometry = GEOSGeometry(feature.geom.wkt, srid=SOURCE_DATA_SRID) + self.address = {"fi": properties["Osoite"]} + self.address["sv"] = properties["Adress"] + self.address["en"] = properties["Address"] + self.geometry = Point( + geometry["coordinates"][0], + geometry["coordinates"][1], + srid=SOURCE_DATA_SRID, + ) self.geometry.transform(settings.DEFAULT_SRID) - for field in feature.fields: + + for field in properties.keys(): if field in self.extra_field_mappings: match self.extra_field_mappings[field]["type"]: + case FieldTypes.MULTILANG_STRING: + self.extra[field] = {} + for lang in LANGUAGES: + key = self.extra_field_mappings[field][lang] + # Support multiple keys for same field, e.g., + # 'Payment method' and 'Payment Method' + if type(key) == list: + for k in key: + val = properties.get(k, None) + if val: + self.extra[field][lang] = val + break + else: + self.extra[field][lang] = properties[key] + case FieldTypes.STRING: - self.extra[field] = feature[field].as_string() + self.extra[field] = properties[field] case FieldTypes.INTEGER: - self.extra[field] = feature[field].as_int() + val = properties[field] + self.extra[field] = int(val) if val else None case FieldTypes.FLOAT: - self.extra[field] = feature[field].as_double() + val = properties[field] + self.extra[field] = float(val) if val else None -def get_data_layer(): +def get_json_data(): file_name = get_file_name_from_data_source(CONTENT_TYPE_NAME) if not file_name: file_name = f"{get_root_dir()}/mobility_data/data/{GEOJSON_FILENAME}" - ds = GDALDataSource(file_name) - assert len(ds) == 1 - return ds[0] + json_data = None + import json + + with open(file_name, "r") as json_file: + json_data = json.loads(json_file.read()) + return json_data def get_parking_machine_objects(): - data_layer = get_data_layer() - return [ParkingMachine(feature) for feature in data_layer] + json_data = get_json_data()["features"] + return [ParkingMachine(feature) for feature in json_data] @db.transaction.atomic def get_and_create_parking_machine_content_type(): - description = "Parking machines in the city of Turku." + description = "Parking machines in the City of Turku." content_type, _ = get_or_create_content_type(CONTENT_TYPE_NAME, description) return content_type @@ -75,12 +140,12 @@ def get_and_create_parking_machine_content_type(): def save_to_database(objects, delete_tables=True): if delete_tables: delete_mobile_units(CONTENT_TYPE_NAME) - content_type = get_and_create_parking_machine_content_type() for object in objects: mobile_unit = MobileUnit.objects.create( - address=object.address, geometry=object.geometry, extra=object.extra, ) mobile_unit.content_types.add(content_type) + set_translated_field(mobile_unit, "address", object.address) + mobile_unit.save() From 7fee69c807192e3eb15f75a974fd3709ef4d4396 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 10:18:48 +0200 Subject: [PATCH 124/188] Add tests for multilingual fields --- .../tests/test_import_parking_machines.py | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/mobility_data/tests/test_import_parking_machines.py b/mobility_data/tests/test_import_parking_machines.py index e65c27cb7..3c69e1c1a 100644 --- a/mobility_data/tests/test_import_parking_machines.py +++ b/mobility_data/tests/test_import_parking_machines.py @@ -4,15 +4,15 @@ from mobility_data.models import ContentType, MobileUnit -from .utils import get_test_fixture_data_layer +from .utils import get_test_fixture_json_data @pytest.mark.django_db -@patch("mobility_data.importers.parking_machines.get_data_layer") -def test_import_parking_machines(get_data_layer_mock): +@patch("mobility_data.importers.parking_machines.get_json_data") +def test_import_parking_machines(get_json_data_mock): from mobility_data.importers import parking_machines - get_data_layer_mock.return_value = get_test_fixture_data_layer( + get_json_data_mock.return_value = get_test_fixture_json_data( "parking_machines.geojson" ) objects = parking_machines.get_parking_machine_objects() @@ -23,15 +23,29 @@ def test_import_parking_machines(get_data_layer_mock): assert satamakatu.content_types.all().count() == 1 assert satamakatu.content_types.first() == ContentType.objects.first() assert satamakatu.address == "Satamakatu 18 vp" + assert satamakatu.address_sv == "Hamngatan 18 me" + assert satamakatu.address_en == "Satamakatu 18 vp" assert satamakatu.extra["Malli"] == "CWT-C Touch" assert satamakatu.extra["Muuta"] == "16 € / 26 h" - assert satamakatu.extra["Virta"] == "Verkkovirta" assert satamakatu.extra["Taksa/h"] == 1.3 assert satamakatu.extra["Max.aika"] is None - assert satamakatu.extra["Näyttö"] == '9", kosketus' - assert satamakatu.extra["Omistaja"] == "Turun kaupunki" - assert satamakatu.extra["Sijainti"] == "Katuosa" assert satamakatu.extra["Asennettu"] == "15.10.2022" - assert satamakatu.extra["Maksutapa"] == "Kolikko, kortti, lähimaksu" assert satamakatu.extra["Valmistaja"] == "Cale" - assert satamakatu.extra["Maksuvyöhyke"] is None + assert satamakatu.extra["Virta"]["fi"] == "Verkkovirta" + assert satamakatu.extra["Virta"]["sv"] == "Nät" + assert satamakatu.extra["Virta"]["en"] == "Mains" + assert satamakatu.extra["Näyttö"]["fi"] == '9", kosketus' + assert satamakatu.extra["Näyttö"]["sv"] == '9", pekskärm' + assert satamakatu.extra["Näyttö"]["en"] == '9", touch screen' + assert satamakatu.extra["Omistaja"]["fi"] == "Turun kaupunki" + assert satamakatu.extra["Omistaja"]["sv"] == "Åbo stad" + assert satamakatu.extra["Omistaja"]["en"] == "City of Turku" + assert satamakatu.extra["Sijainti"]["fi"] == "Katuosa" + assert satamakatu.extra["Sijainti"]["sv"] == "Gata" + assert satamakatu.extra["Sijainti"]["en"] == "On-street" + assert satamakatu.extra["Maksuvyöhyke"]["fi"] == "Satama" + assert satamakatu.extra["Maksuvyöhyke"]["sv"] == "Hamn" + assert satamakatu.extra["Maksuvyöhyke"]["en"] == "Harbour" + assert satamakatu.extra["Maksutapa"]["fi"] == "Kolikko, kortti, lähimaksu" + assert satamakatu.extra["Maksutapa"]["sv"] == "Mynt, kort, kontaktlös" + assert satamakatu.extra["Maksutapa"]["en"] == "Coin, card, contactless" From 34b307175135dc4e3e15bee9ba3fcb87e0b21072 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 12:18:31 +0200 Subject: [PATCH 125/188] Change results to result --- mobility_data/tests/test_api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index 825cc8116..04a28fe7d 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -36,7 +36,7 @@ def test_mobile_unit(api_client, mobile_units, content_types): assert result["extra"]["test_string"] == "4242" assert result["extra"]["test_int"] == 4242 assert result["extra"]["test_float"] == 42.42 - assert results["geometry"] == Point( + assert result["geometry"] == Point( 235404.6706163187, 6694437.919005549, srid=settings.DEFAULT_SRID ) # Test multiple content types @@ -68,8 +68,8 @@ def test_mobile_unit(api_client, mobile_units, content_types): ) response = api_client.get(url) assert response.status_code == 200 - result = response.json()["results"][1] - assert results["name"] == "Test mobileunit" + result = response.json()["results"][1] + assert result["name"] == "Test mobileunit" # Test that we get a mobile unit inside bbox. url = ( reverse("mobility_data:mobile_units-list") From 5b4c93f07ffe60beb33dbe1d10576f0180c4d741 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 14:31:14 +0200 Subject: [PATCH 126/188] Add support for bool value to extra__ param --- mobility_data/api/views.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mobility_data/api/views.py b/mobility_data/api/views.py index cf5ff3206..ba554115f 100644 --- a/mobility_data/api/views.py +++ b/mobility_data/api/views.py @@ -224,10 +224,14 @@ def list(self, request): f"extra field '{key}' does not exist", status=status.HTTP_400_BAD_REQUEST, ) + if field_type == float: value = float(value) elif field_type == int: value = int(value) + elif field_type == bool: + value = strtobool(value) + value = bool(value) queryset = queryset.filter(**{filter: value}) page = self.paginate_queryset(queryset) From ba4fadbb8f4b866374cbd9fe799c63adab82a9c3 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 14:37:08 +0200 Subject: [PATCH 127/188] Add extra field with identical keys to the mobile unit(same content type) --- mobility_data/tests/conftest.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mobility_data/tests/conftest.py b/mobility_data/tests/conftest.py index 09c33cffd..8105bff3c 100644 --- a/mobility_data/tests/conftest.py +++ b/mobility_data/tests/conftest.py @@ -70,7 +70,12 @@ def group_type(): @pytest.fixture def mobile_units(content_types): mobile_units = [] - extra = {"test_int": 4242, "test_float": 42.42, "test_string": "4242"} + extra = { + "test_int": 4242, + "test_float": 42.42, + "test_string": "4242", + "test_bool": False, + } geometry = Point(22.21, 60.3, srid=4326) geometry.transform(settings.DEFAULT_SRID) mobile_unit = MobileUnit.objects.create( @@ -81,10 +86,17 @@ def mobile_units(content_types): ) mobile_unit.content_types.add(content_types[0]) mobile_units.append(mobile_units) + extra = { + "test_int": 14, + "test_float": 2.4, + "test_string": "hello", + "test_bool": True, + } mobile_unit = MobileUnit.objects.create( name="Test2 mobileunit", description="Test2 description", geometry=Point(43.43, 22.22, srid=settings.DEFAULT_SRID), + extra=extra, ) mobile_unit.content_types.add(content_types[0]) mobile_unit.content_types.add(content_types[1]) From 10e41918a83cd9ab5d92d35dcee51d7912341b25 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 13 Feb 2023 14:38:55 +0200 Subject: [PATCH 128/188] Fix faulty tests and add test for bool extra value --- mobility_data/tests/test_api.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index 04a28fe7d..e9123f58c 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -44,32 +44,42 @@ def test_mobile_unit(api_client, mobile_units, content_types): assert len(result["content_types"]) == 2 assert result["content_types"][0]["name"] == "Test" assert result["content_types"][1]["name"] == "Test2" + # Test string in extra field url = ( reverse("mobility_data:mobile_units-list") - + "?type_name=Test?extra__test_string=4242" + + "?type_name=Test&extra__test_string=4242" ) response = api_client.get(url) assert response.status_code == 200 - result = response.json()["results"][1] - assert result["name"] == "Test mobileunit" + assert len(response.json()["results"]) == 1 + assert response.json()["results"][0]["name"] == "Test mobileunit" # Test int value in extra field url = ( reverse("mobility_data:mobile_units-list") - + "?type_name=Test?extra__test_int=4242" + + "?type_name=Test&extra__test_int=4242" ) response = api_client.get(url) assert response.status_code == 200 - result = response.json()["results"][1] - assert result["name"] == "Test mobileunit" + assert len(response.json()["results"]) == 1 + assert response.json()["results"][0]["name"] == "Test mobileunit" # Test float value in extra field url = ( reverse("mobility_data:mobile_units-list") - + "?type_name=Test?extra__test_float=42.42" + + "?type_name=Test&extra__test_float=42.42" ) response = api_client.get(url) assert response.status_code == 200 - result = response.json()["results"][1] - assert result["name"] == "Test mobileunit" + assert len(response.json()["results"]) == 1 + assert response.json()["results"][0]["name"] == "Test mobileunit" + # Test vool value in extra field + url = ( + reverse("mobility_data:mobile_units-list") + + "?type_name=Test&extra__test_bool=True" + ) + response = api_client.get(url) + assert response.status_code == 200 + assert len(response.json()["results"]) == 1 + assert response.json()["results"][0]["name"] == "Test2 mobileunit" # Test that we get a mobile unit inside bbox. url = ( reverse("mobility_data:mobile_units-list") From 513a15ba59ae0be3e1748d6ca27c79ee421d5085 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 11:50:56 +0200 Subject: [PATCH 129/188] Add migration that adds field original_event_names --- ...ancework_add_field_original_event_names.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 street_maintenance/migrations/0012_maintenancework_add_field_original_event_names.py diff --git a/street_maintenance/migrations/0012_maintenancework_add_field_original_event_names.py b/street_maintenance/migrations/0012_maintenancework_add_field_original_event_names.py new file mode 100644 index 000000000..f15f279e8 --- /dev/null +++ b/street_maintenance/migrations/0012_maintenancework_add_field_original_event_names.py @@ -0,0 +1,21 @@ +# Generated by Django 4.1.2 on 2023-02-16 11:35 + +import django.contrib.postgres.fields +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ("street_maintenance", "0011_rename_provider_autori_to_yit"), + ] + + operations = [ + migrations.AddField( + model_name="maintenancework", + name="original_event_names", + field=django.contrib.postgres.fields.ArrayField( + base_field=models.CharField(max_length=64), default=list, size=None + ), + ), + ] From 55efff9e864bb0f588c715fa422b10bceefaecc4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 11:51:37 +0200 Subject: [PATCH 130/188] Add field original_event_names --- street_maintenance/models.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/street_maintenance/models.py b/street_maintenance/models.py index bc466a4e6..2f0ad44b3 100644 --- a/street_maintenance/models.py +++ b/street_maintenance/models.py @@ -19,6 +19,8 @@ def __str__(self): class MaintenanceWork(models.Model): geometry = models.GeometryField(srid=DEFAULT_SRID, null=True) events = ArrayField(models.CharField(max_length=64), default=list) + original_event_names = ArrayField(models.CharField(max_length=64), default=list) + timestamp = models.DateTimeField() maintenance_unit = models.ForeignKey( "MaintenanceUnit", From 5786e15adcd1c24c748ff8cbca71b6543d372844 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 11:52:13 +0200 Subject: [PATCH 131/188] Serialize field original_event_names --- street_maintenance/api/serializers.py | 1 + 1 file changed, 1 insertion(+) diff --git a/street_maintenance/api/serializers.py b/street_maintenance/api/serializers.py index 965a119cc..6ae513acd 100644 --- a/street_maintenance/api/serializers.py +++ b/street_maintenance/api/serializers.py @@ -51,6 +51,7 @@ class Meta: "geometry", "timestamp", "events", + "original_event_names", ] def to_representation(self, obj): From cebbeb2523a2041fe17748ae622bf3ccf3c636ab Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 11:52:41 +0200 Subject: [PATCH 132/188] Add event 'Kelintarkastus' --- street_maintenance/management/commands/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/street_maintenance/management/commands/constants.py b/street_maintenance/management/commands/constants.py index 698818ec6..301e78bef 100644 --- a/street_maintenance/management/commands/constants.py +++ b/street_maintenance/management/commands/constants.py @@ -130,6 +130,7 @@ "pysäkkikatosten hoito": [MUUT], "liikennemerkkien puhdistus": [MUUT], "siirtoajo": [MUUT], + "Kelintarkastus": [MUUT], } TIMESTAMP_FORMATS = { INFRAROAD: "%Y-%m-%d %H:%M:%S", From 5973b50e2af1cdbf3bfa1a1b53ceb4f2e5a0b6d8 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 12:05:35 +0200 Subject: [PATCH 133/188] Delete only works --- .../commands/import_destia_street_maintenance_history.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/street_maintenance/management/commands/import_destia_street_maintenance_history.py b/street_maintenance/management/commands/import_destia_street_maintenance_history.py index b000ef7fa..78e5a6d1e 100644 --- a/street_maintenance/management/commands/import_destia_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_destia_street_maintenance_history.py @@ -1,6 +1,6 @@ import logging -from street_maintenance.models import MaintenanceUnit +from street_maintenance.models import MaintenanceWork from .base_import_command import BaseImportCommand from .constants import ( @@ -41,7 +41,7 @@ def add_arguments(self, parser): def handle(self, *args, **options): super().__init__() - MaintenanceUnit.objects.filter(provider=DESTIA).delete() + MaintenanceWork.objects.filter(maintenance_unit__provider=DESTIA).delete() if options["history_size"]: history_size = options["history_size"][0] else: From 60a5423c9803e48d82f2111fb6c1158c6be20ae8 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 12:06:41 +0200 Subject: [PATCH 134/188] Create Unit only if new, assign original_event_names --- .../management/commands/utils.py | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index 0a6fabe49..044d5a3de 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -277,6 +277,7 @@ def create_maintenance_works(provider, history_size, fetch_size): maintenance_unit=unit, geometry=point, events=events, + original_event_names=work["events"], ) ) MaintenanceWork.objects.bulk_create(works) @@ -285,6 +286,7 @@ def create_maintenance_works(provider, history_size, fetch_size): def create_maintenance_units(provider): + num_created = 0 assert provider in PROVIDERS response = requests.get(URLS[provider][UNITS]) assert ( @@ -292,15 +294,27 @@ def create_maintenance_units(provider): ), "Fetching Maintenance Unit {} status code: {}".format( URLS[provider][UNITS], response.status_code ) + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=provider).values_list("id", flat=True) + ) for unit in response.json(): # The names of the unit is derived from the events. names = [n for n in unit["last_location"]["events"]] - MaintenanceUnit.objects.create( + obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit["id"], names=names, provider=provider ) - num_units_imported = MaintenanceUnit.objects.filter(provider=provider).count() - logger.info(f"Imported {num_units_imported} {provider} mainetance Units.") - return num_units_imported + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + + MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() + num_units = MaintenanceUnit.objects.filter(provider=provider).count() + logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {provider}") + logger.info( + f"Created {num_created} units of total {num_units} units for provied {provider}." + ) + return num_units def get_yit_contract(access_token): @@ -343,6 +357,11 @@ def create_kuntec_maintenance_units(): units_url, response.status_code ) no_io_din = 0 + num_created = 0 + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=KUNTEC).values_list("id", flat=True) + ) + for unit in response.json()["data"]["units"]: names = [] if "io_din" in unit: @@ -355,17 +374,24 @@ def create_kuntec_maintenance_units(): # If names, we have a unit with at least one io_din with State On. if len(names) > 0: unit_id = unit["unit_id"] - MaintenanceUnit.objects.create( + obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit_id, names=names, provider=KUNTEC ) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + else: no_io_din += 1 + MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() + logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {KUNTEC}") logger.info( f"Discarding {no_io_din} Kuntec units that do not have a io_din with Status 'On'(1)." ) + num_units = MaintenanceUnit.objects.filter(provider=KUNTEC).count() logger.info( - f"Imported {MaintenanceUnit.objects.filter(provider=KUNTEC).count()}" - + " Kuntec mainetance Units." + f"Created {num_created} units of total {num_units} units for provied {KUNTEC}." ) @@ -378,12 +404,24 @@ def create_yit_maintenance_units(access_token): ), " Fetching YIT vehicles {} failed, status code: {}".format( URLS[YIT][VEHICLES], response.status_code ) + num_created = 0 + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=YIT).values_list("id", flat=True) + ) for unit in response.json(): names = [unit["vehicleTypeName"]] - MaintenanceUnit.objects.create(unit_id=unit["id"], names=names, provider=YIT) + obj, created = MaintenanceUnit.objects.get_or_create( + unit_id=unit["id"], names=names, provider=YIT + ) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() + logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {YIT}") + num_units = MaintenanceUnit.objects.filter(provider=YIT).count() logger.info( - f"Imported {MaintenanceUnit.objects.filter(provider=YIT).count()}" - + " YIT mainetance Units." + f"Created {num_created} units of total {num_units} units for provied {YIT}." ) @@ -416,6 +454,10 @@ def get_yit_routes(access_token, contract, history_size): def get_yit_access_token(): + """ + Note the IP address of the host calling Autori API (hosts YIT data) must be + given for whitelistning. + """ assert settings.YIT_SCOPE, "YIT_SCOPE not defined in environment." assert settings.YIT_CLIENT_ID, "YIT_CLIENT_ID not defined in environment." assert settings.YIT_CLIENT_SECRET, "YIT_CLIENT_SECRET not defined in environment." From 0ab72e1e6bcfe6b4fda71d3829836dc7c2c386bb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:15:17 +0200 Subject: [PATCH 135/188] Not delete works before importing --- .../commands/import_destia_street_maintenance_history.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/street_maintenance/management/commands/import_destia_street_maintenance_history.py b/street_maintenance/management/commands/import_destia_street_maintenance_history.py index 78e5a6d1e..cab1c05b3 100644 --- a/street_maintenance/management/commands/import_destia_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_destia_street_maintenance_history.py @@ -1,7 +1,5 @@ import logging -from street_maintenance.models import MaintenanceWork - from .base_import_command import BaseImportCommand from .constants import ( DESTIA, @@ -41,7 +39,6 @@ def add_arguments(self, parser): def handle(self, *args, **options): super().__init__() - MaintenanceWork.objects.filter(maintenance_unit__provider=DESTIA).delete() if options["history_size"]: history_size = options["history_size"][0] else: From 755d78788922aad9dcfec825b320a10e9a395707 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:16:16 +0200 Subject: [PATCH 136/188] Not delete works and units before importing --- .../commands/import_infraroad_street_maintenance_history.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py b/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py index ffd7854f0..cb4b2b2a8 100644 --- a/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py @@ -1,7 +1,5 @@ import logging -from street_maintenance.models import MaintenanceUnit - from .base_import_command import BaseImportCommand from .constants import ( INFRAROAD, @@ -42,7 +40,6 @@ def add_arguments(self, parser): def handle(self, *args, **options): super().__init__() - MaintenanceUnit.objects.filter(provider=INFRAROAD).delete() if options["history_size"]: history_size = options["history_size"][0] else: From 9ff021e377ff74e75ca6025e6548ad59651aa2c6 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:18:40 +0200 Subject: [PATCH 137/188] Create only new works, delete obsolete --- ...mport_kuntec_street_maintenance_history.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py b/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py index d87a2ac38..c9875645b 100644 --- a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py @@ -42,10 +42,13 @@ def add_arguments(self, parser): ) def create_kuntec_maintenance_works(self, history_size=None): - works = [] + num_created = 0 now = datetime.now() start = (now - timedelta(days=history_size)).strftime(TIMESTAMP_FORMATS[KUNTEC]) end = now.strftime(TIMESTAMP_FORMATS[KUNTEC]) + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=KUNTEC).values_list("id", flat=True) + ) for unit in MaintenanceUnit.objects.filter(provider=KUNTEC): url = URLS[KUNTEC][WORKS].format( key=KUNTEC_KEY, start=start, end=end, unit_id=unit.unit_id @@ -57,6 +60,7 @@ def create_kuntec_maintenance_works(self, history_size=None): for units in response.json()["data"]["units"]: for route in units["routes"]: events = [] + original_event_names = [] # Routes of type 'stop' are discarded. if route["type"] == "route": # Check for mapped events to include as works. @@ -67,6 +71,7 @@ def create_kuntec_maintenance_works(self, history_size=None): # If mapping value is None, the event is not used. if e: events.append(e) + original_event_names.append(name) else: logger.warning( f"Found unmapped event: {event_name}" @@ -86,22 +91,35 @@ def create_kuntec_maintenance_works(self, history_size=None): if not geometry: continue timestamp = route["start"]["time"] - works.append( - MaintenanceWork( - timestamp=timestamp, - maintenance_unit=unit, - events=events, - geometry=geometry, - ) + + obj, created = MaintenanceWork.get_or_create( + timestamp=timestamp, + maintenance_unit=unit, + events=events, + original_event_names=original_event_names, + geometry=geometry, ) - MaintenanceWork.objects.bulk_create(works) - logger.info(f"Imported {len(works)} Kuntec maintenance works.") - return len(works) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + + MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() + num_works = MaintenanceWork.objects.filter( + maintenance_unit__provider=KUNTEC + ).count() + logger.info( + f"Deleted {len(ids_to_delete)} obsolete Works for provider {KUNTEC}" + ) + logger.info( + f"Created {num_created} Works of total {num_works} Works for provider {KUNTEC}." + ) + return num_created def handle(self, *args, **options): super().__init__() assert settings.KUNTEC_KEY, "KUNTEC_KEY not found in environment." - MaintenanceUnit.objects.filter(provider=KUNTEC).delete() + MaintenanceWork.objects.filter(maintenance_unit__provider=KUNTEC).delete() history_size = KUNTEC_DEFAULT_WORKS_HISTORY_SIZE if options["history_size"]: history_size = int(options["history_size"][0]) From cbe9a63cb72d11a41db07dbe13b588ef83547815 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:19:50 +0200 Subject: [PATCH 138/188] Create only new works and delete obsolete --- .../import_yit_street_maintenance_history.py | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/street_maintenance/management/commands/import_yit_street_maintenance_history.py b/street_maintenance/management/commands/import_yit_street_maintenance_history.py index eccdd3c52..34e4a9623 100644 --- a/street_maintenance/management/commands/import_yit_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_yit_street_maintenance_history.py @@ -45,7 +45,10 @@ def create_yit_maintenance_works(self, history_size=None): list_of_events = get_yit_event_types(access_token) event_name_mappings = create_dict_from_yit_events(list_of_events) routes = get_yit_routes(access_token, contract, history_size) - works = [] + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=YIT).values_list("id", flat=True) + ) + num_created = 0 for route in routes: if len(route["geography"]["features"]) > 1: logger.warning( @@ -63,6 +66,7 @@ def create_yit_maintenance_works(self, history_size=None): if not geometry: continue events = [] + original_event_names = [] operations = route["operations"] for operation in operations: event_name = event_name_mappings[operation].lower() @@ -71,6 +75,7 @@ def create_yit_maintenance_works(self, history_size=None): # If mapping value is None, the event is not used. if e: events.append(e) + original_event_names.append(event_name_mappings[operation]) else: logger.warning( f"Found unmapped event: {event_name_mappings[operation]}" @@ -89,22 +94,31 @@ def create_yit_maintenance_works(self, history_size=None): except MaintenanceUnit.DoesNotExist: logger.warning(f"Maintenance unit: {unit_id}, not found.") continue - works.append( - MaintenanceWork( - timestamp=route["startTime"], - maintenance_unit=unit, - events=events, - geometry=geometry, - ) - ) - MaintenanceWork.objects.bulk_create(works) - logger.info(f"Imported {len(works)} YIT mainetance works.") - return len(works) + obj, created = MaintenanceWork.objects.get_or_create( + timestamp=route["startTime"], + maintenance_unit=unit, + events=events, + original_event_names=original_event_names, + geometry=geometry, + ) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() + num_works = MaintenanceWork.objects.filter( + maintenance_unit__provider=YIT + ).count() + logger.info(f"Deleted {len(ids_to_delete)} obsolete Works for provider {YIT}") + logger.info( + f"Created {num_created} Works of total {num_works} Works for provider {YIT}." + ) + return num_created def handle(self, *args, **options): super().__init__() - MaintenanceUnit.objects.filter(provider=YIT).delete() + MaintenanceWork.objects.filter(maintenance_unit__provider=YIT).delete() history_size = YIT_DEFAULT_WORKS_HISTORY_SIZE if options["history_size"]: history_size = int(options["history_size"][0]) From c7e9c60c03fa674dae078c6c54f64e3895dd28d6 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:22:12 +0200 Subject: [PATCH 139/188] Create only new works and delete obsolete works --- .../management/commands/utils.py | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index 044d5a3de..fe66d6d56 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -228,7 +228,8 @@ def get_linestring_in_boundary(linestring, boundary): def create_maintenance_works(provider, history_size, fetch_size): turku_boundary = get_turku_boundary() - works = [] + num_created = 0 + import_from_date_time = datetime.now() - timedelta(days=history_size) import_from_date_time = import_from_date_time.replace( tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki") @@ -242,6 +243,11 @@ def create_maintenance_works(provider, history_size, fetch_size): else: logger.warning(f"Location history not found for unit: {unit.unit_id}") continue + ids_to_delete = list( + MaintenanceUnit.objects.filter(provider=provider).values_list( + "id", flat=True + ) + ) for work in json_data: timestamp = datetime.strptime( @@ -259,6 +265,7 @@ def create_maintenance_works(provider, history_size, fetch_size): continue events = [] + original_event_names = [] for event in work["events"]: event_name = event.lower() if event_name in EVENT_MAPPINGS: @@ -266,23 +273,32 @@ def create_maintenance_works(provider, history_size, fetch_size): # If mapping value is None, the event is not used. if e: events.append(e) + original_event_names.append(event) else: logger.warning(f"Found unmapped event: {event}") # If no events found discard the work if len(events) == 0: continue - works.append( - MaintenanceWork( - timestamp=timestamp, - maintenance_unit=unit, - geometry=point, - events=events, - original_event_names=work["events"], - ) + obj, created = MaintenanceWork.objects.get_or_create( + timestamp=timestamp, + maintenance_unit=unit, + geometry=point, + events=events, + original_event_names=original_event_names, ) - MaintenanceWork.objects.bulk_create(works) - logger.info(f"Imported {len(works)} {provider} mainetance works.") - return len(works) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() + num_works = MaintenanceWork.objects.filter( + maintenance_unit__provider=provider + ).count() + logger.info(f"Deleted {len(ids_to_delete)} obsolete Works for provider {provider}") + logger.info( + f"Created {num_created} Works of total {num_works} Works for provider {provider}." + ) + return num_created def create_maintenance_units(provider): @@ -312,7 +328,7 @@ def create_maintenance_units(provider): num_units = MaintenanceUnit.objects.filter(provider=provider).count() logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {provider}") logger.info( - f"Created {num_created} units of total {num_units} units for provied {provider}." + f"Created {num_created} units of total {num_units} units for provider {provider}." ) return num_units @@ -391,7 +407,7 @@ def create_kuntec_maintenance_units(): ) num_units = MaintenanceUnit.objects.filter(provider=KUNTEC).count() logger.info( - f"Created {num_created} units of total {num_units} units for provied {KUNTEC}." + f"Created {num_created} units of total {num_units} units for provider {KUNTEC}." ) @@ -421,7 +437,7 @@ def create_yit_maintenance_units(access_token): logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {YIT}") num_units = MaintenanceUnit.objects.filter(provider=YIT).count() logger.info( - f"Created {num_created} units of total {num_units} units for provied {YIT}." + f"Created {num_created} units of total {num_units} units for provider {YIT}." ) From ec2d4759ee921b674266919b3f468434cbeecf87 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 17 Feb 2023 13:51:38 +0200 Subject: [PATCH 140/188] Remove obsolete newline --- street_maintenance/models.py | 1 - 1 file changed, 1 deletion(-) diff --git a/street_maintenance/models.py b/street_maintenance/models.py index 2f0ad44b3..9ce318535 100644 --- a/street_maintenance/models.py +++ b/street_maintenance/models.py @@ -20,7 +20,6 @@ class MaintenanceWork(models.Model): geometry = models.GeometryField(srid=DEFAULT_SRID, null=True) events = ArrayField(models.CharField(max_length=64), default=list) original_event_names = ArrayField(models.CharField(max_length=64), default=list) - timestamp = models.DateTimeField() maintenance_unit = models.ForeignKey( "MaintenanceUnit", From 6fdca42608b21b85a327221d5731a968fee3b50e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 20 Feb 2023 13:45:48 +0200 Subject: [PATCH 141/188] Get check_turku_bondary value with default value --- smbackend_turku/importers/divisions.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/smbackend_turku/importers/divisions.py b/smbackend_turku/importers/divisions.py index 0cd27a6e2..88ed163b4 100644 --- a/smbackend_turku/importers/divisions.py +++ b/smbackend_turku/importers/divisions.py @@ -46,9 +46,7 @@ def __init__(self, logger=None, importer=None): self.muni_data_path = "data" def _import_division(self, muni, div, type_obj, syncher, parent_dict, feat): - check_turku_boundary = True - if "check_turku_boundary" in div: - check_turku_boundary = div["check_turku_boundary"] + check_turku_boundary = div.get("check_turku_boundary", True) geom = feat.geom if not geom.srid: geom.srid = SOURCE_DATA_SRID From 9f98da7bc3016678111a5feba1f169bc8e47e888 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 20 Feb 2023 13:46:51 +0200 Subject: [PATCH 142/188] Do not check the boundarys of Turku for swedish school districts --- smbackend_turku/importers/data/divisions_config.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/smbackend_turku/importers/data/divisions_config.yml b/smbackend_turku/importers/data/divisions_config.yml index 2caec23ee..e79b87866 100644 --- a/smbackend_turku/importers/data/divisions_config.yml +++ b/smbackend_turku/importers/data/divisions_config.yml @@ -79,6 +79,7 @@ divisions: name: "Oppilaaksiottoalue, ruotsinkielinen alakoulu" ocd_id: oppilaaksiottoalue_alakoulu_sv wfs_layer: 'GIS:Oppilasalueet_ruotsi_1-6' + check_turku_boundary: False fields: name: sv: Oppilasalueen_kuvaus @@ -89,6 +90,7 @@ divisions: name: "Oppilaaksiottoalue, ruotsinkielinen yläkoulu" ocd_id: oppilaaksiottoalue_ylakoulu_sv wfs_layer: 'GIS:Oppilasalueet_ruotsi_7-9' + check_turku_boundary: False fields: name: sv: Oppilasalueen_kuvaus From b3c6c43885bb48ef34e9c2014159e11604768ee4 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 21 Feb 2023 08:51:59 +0200 Subject: [PATCH 143/188] =?UTF-8?q?Add=20event=20named=20'Sulamisvesien=20?= =?UTF-8?q?hallinta=20/=20h=C3=B6yrytys'?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- street_maintenance/management/commands/constants.py | 1 + 1 file changed, 1 insertion(+) diff --git a/street_maintenance/management/commands/constants.py b/street_maintenance/management/commands/constants.py index 301e78bef..10377d001 100644 --- a/street_maintenance/management/commands/constants.py +++ b/street_maintenance/management/commands/constants.py @@ -131,6 +131,7 @@ "liikennemerkkien puhdistus": [MUUT], "siirtoajo": [MUUT], "Kelintarkastus": [MUUT], + "Sulamisvesien hallinta / höyrytys": [MUUT], } TIMESTAMP_FORMATS = { INFRAROAD: "%Y-%m-%d %H:%M:%S", From 9ac6364860bb7db1cb938a6b85cb1bf2d99b352f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 21 Feb 2023 08:52:53 +0200 Subject: [PATCH 144/188] Add objects manager when calling get_or_create --- .../commands/import_kuntec_street_maintenance_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py b/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py index c9875645b..afedebd22 100644 --- a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py @@ -92,7 +92,7 @@ def create_kuntec_maintenance_works(self, history_size=None): continue timestamp = route["start"]["time"] - obj, created = MaintenanceWork.get_or_create( + obj, created = MaintenanceWork.objects.get_or_create( timestamp=timestamp, maintenance_unit=unit, events=events, From 100a3aee55cf1d5d072a0f64585138fa19d59215 Mon Sep 17 00:00:00 2001 From: Juuso Jokiniemi <68938778+juuso-j@users.noreply.github.com> Date: Thu, 23 Feb 2023 12:16:25 +0200 Subject: [PATCH 145/188] Add street maintenance kwargs screenshots --- .../street_maintenance_kwargs_example.PNG | Bin 0 -> 19819 bytes ..._maintenance_kwargs_example_one_provider.PNG | Bin 0 -> 19383 bytes 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 wiki_images/street_maintenance_kwargs_example.PNG create mode 100644 wiki_images/street_maintenance_kwargs_example_one_provider.PNG diff --git a/wiki_images/street_maintenance_kwargs_example.PNG b/wiki_images/street_maintenance_kwargs_example.PNG new file mode 100644 index 0000000000000000000000000000000000000000..07642f52dfb04654beb338f6eaa80a89388e3f8b GIT binary patch literal 19819 zcmeIad03L$-|%bO-JDw6N^`8;PIqYzDb7k;+gUl5mb0do0}eP(h}KS}GPSmuiXxe_ z=9DQ;V2Np^;s7|I5SbDnq9LLvaA^1Z&Uv46o%edqdp+lmbG_H^4-xMBX0h(I)^~lU z&-Z@dXm2gGPhp>ggoM=POBb$5NJs)CBz`)vXP5X${OnVr_}`A`Yu1($4LBvb_`}bE zf7tyYA<>+(f7^Gb`19U~OK#B;62HWJ|Jy+hP)(4Ku#>oa;Sc8oZ`Pa$>_tRqZ_`pg zI&AA)t9Wu|SVmB@9i`z_6Z9M9Yjd^dmwmoTm#b3RrJn4mMgFP*Kj^z~;%ou^=e_yC zjvt=FZ4gn|pHaW*9o(mNxkxQNvigMXt%;Ti@>X%n$g0Ue+%La0e&N&~ne|5Q`5=ezlZ_K-P<#ffKrXW5dKI}Ypitzi7w-2>SFMhx7 z%IWJddjbD^4Mla$X>5VkKUrH!>y`~RdE6r!OUQ4vN~r2;b^FGd>zf1T-i(&0J_Q3oU7?@?cUv@ zRr`$(igtqa5$bFpec1sEzIHxY+}SAi1{!05Uzg*Tu>HA_>O#~<0V^ravRl#X&7p#A z?8;l(iR3kE;r9HGk%I1x9kd$8#ETSsxWOBrw415IrTGliiDk?>ISdqphHjO6PQDSN(R)RO| zDEh#?`M^f?Ll!;1;54;i=|FF~UAzFlLNfGt`)Nb97p$E+W+u;>oB?eRMGdMpFt>EB zLv3U-IZ+UZbk1?fX^rhK#N}*$%_MUs8`I)N{_jsyZ3Y###**WWyI0;;QCo1D!pT;0h{^Tzmm!99 zv*SOOph2UDBIqb{*Y<4JX|r4F7h~XbZ9lB&ZN&;$lwz=CD!6hY zNw$IJ#WlFH>7;7ojV*`ysI1a2v#PaL)7}ZZT#zwzohH+Z3EdvY44GZ@YSA z97GJ(iob6)GdEf+i@T|FTqDH(9`Ib%f6S{3N}u2iQ)|V^ZdL)`E%P`Iy&4Fb z^Nw_ObjR#G1;9f{Umcoxw6r-B6=~1++)7%N;GP0 z$dIcgDOwGKZ{WX(#z}R}8-FAS?g3#g_G=~h8lld(Ghp$Ni2rLwA5H~Xp8=1r9lgQQ zW!eHK@U+kTOM}ndfcHuRD2>9C+qdYfi4eGi>^(=CG5x3AJzklO9&=>hkRJ=&3q05B z=)@N65lv4Q9b$^gu`f*^;h$MjI~I7fUmK+7v*HRIrgu;7zwMv#Leg z4I4t{0Xv_9HF7!TcCkb0{=@oc1K+R;M_Fd4rw-EXeb;W%IX zV-t=IVIEC~1D#WQ%Goa@5FjlTM?`4J{^LYw^JSH|(mRW6@|yij&ta`{^7ci`;SqoT z5^0EsNPpWS)=y+go$An_at?n?>Bw`$Kcbfb0kr3Y)JuBF7CPt{;V20m91~*d0%X>S zXWS=H>#(Et2l@a*QG&7D)p4|)J zQYw#)zZuBrG4VXj%KEVL@z5y*a3dtOI>E%gXKO=l_*5zQH$pj|LcEYgzKyY zM)RV=XSPq<>MvqS+ z+_uP@AqUQ*HQwc2<(3AjLXEz#**|m|_Y?sq0}u%>Z)|7AL?xMgsp0XQwM46-!p(SF z^sy8!a8&|f%d3!QKV%r;xbt7?^r?dP|ALnMUst@H)bBP(gz10F6c zl!P4;G3Df&RjlSipgX1G;Q^2!i92ls#c~y2^?#16wn#|ai*wI8PLQw?_w{_XqvT;L zvEdYzrtn-|Y$H8BNsxGV5F~L&sdU%x{QP5N`SgW|&G~mL1>!M?H!EFih&eZuos~YFJ8buR--P9`EY&&Mv@g~C z1VQ{&hiYSE#eTng6ql|6ECI9G)IDQHyXfV$%(>rhppA*zc^V3(VJB}h-AioZsrCGT`DNiDn3fpZv@$NDj z+9PQnR>=@8p^*jqyx?L7z1Zv^lEh0WUgSVL@J1@QMx@L@U?t-=@)_&yJMydhHynmx z=;0{&)SD(li1sP7@gbT<meM)x{JI1F99{I>U{~lznz4gb9e@KUOi4pt` z`c(I_)Ic4_69-GDPkpi7!$3LHk* z#hK$$fYz*G!TI<1OK+PPtl&ow(sFfJ>QU^FWF)_t-au~H2Z-e7w)jR?sEn)@>X)3P zh9+%)yjDQ(P6;!A>7yV&a@2B|&(!k`_0!U~)+KHSXda~xJ*@wc zB>0(#zRK99Wr}RJrlHSBRexfriBbsieJU%|=-VgPDUKg#EFb%p5Y#g>cjT9ek+ZN0 zL%(Qbc&Hws*g{G2W7z;-TXO~XL}xk}Q+F}>+>p!O%CSe0RLgB%E4Qhw^XP;}R=fxj zeiSr;{cIb$g!B#%Z_Mz|>6hSmvZdW?5JOrrok({HX@lm=(3%xxr4pxmi` z=?Y#ArNzAAR&X09!5le<4*S4ZZ89XsH{rSAdjLf*ZNfGFLjc~4rIy}ly_+fd1u!3NhI0L=(==zIE9;J)Pt_im^ZP88CxulWl(fihtyn1xB=1`mrFa%Ur zS~;VKpC(=PfF^dLXlgS?GO_YgTUSzZxw{oLc_`+W-z&bl!xh1NiUTnAE6rh-8%`2J|A!yCQvKj8 z_fz^$&!pcy9_?^^leZADa##LmX-%Sfe!SLiE2GJe_Kcl>?+U#QmM59ZW)B2kH$8kV zD^4W-O7C0t#4vrSA4|LYhYN zYDzh#F9O=j?_Y#y&(FZPyf;a&TCH+Y|2Q%m}FWRKRFAH-VOaZ$l0QrpjQ z*NNWV!@C|EF@!cI1GjbZ*Oy}~?H=74r2jHHcRS%XCDO-JNb5BCm(LGQ&e@n$*%+{) zDaJ2v41)HTs%3AkMRRZ0Ff;dGI5|;aV?y!v*bnHc<@*2`0*~q)cT9^MvBN~v?lNe_ zx%Nm?*6yPcO?rC(v-b&lcrM2UsSm*t_iGLL2zsF(^;NR;) zY*zkTM*c4o`+tkExD^`uvR3*(W;W)hEU`}^b`b7#d8iL{^BHx+=79e=7vJc6S554P zeBVbS;uVer{qnC9NYCf=*-7zc`Q7>Ww0!Q&s90E@eAhgXNBi{e!AkdvWrv)^tY4W9 zL;_F}cT9O>1iXR46&Oo0lfqPH%Uq4 z%?u=GEIjUagTW6+$pxxmXGwI5gt?{EtQyNPdcE+0j&8J>r!8bhL%mDSlDZ09MuS*( zLTu@(ipKnmQcscZ=QMWfC@YqhQIlo;Y8Oonb~%k8>ebr>Rt^|w!s4nQjEAV2-vG)zKOvru@sQDp-qnG5?Il^RpfxXI zdvvU+I3>i#?SP*D7S@j<{nwgJ%8RYA+r(sGr{U7vTzJMseUC{TY+f(d*9?j`?*J+R zCuflgFQ?0>8?w1vRFw=aJ-42R-k1q3SXw@7LYBJj3q7U8v}Fs;eV6;*E#3_ZGgeBx z)eR?qk&X6U@2c08R0<_Ubyu{spQzJnPWsH!!dpo-jYjd5{E-F|Y?w*x>XI$8IY zFp56CMSFz!ZWacNOf9cA(Fzw59M0C~6U?}*BSCN+&Ofh|N+Zw}KIwGmy`Gd|pKbeJD9piJU2HeqU?Uo-tBC%ud4>B9Ad0d)_UR>f_d zBhnU=g(DxDM0t4X8VACRd`H8@8VL1eC9E*O(Q(TO1&K{9Xx;?zTWPb-s_m8y z?4o;ky`!EYjs4|rVUsKLu5Qv&3k=^GZsDM45y;|tk>tncW2F0d4VHuV=g-W~x2VrY zJ4swY;UNHI$7_}cH{nPb{6uutEd7~GMj z&~7&)d50kAbg`fKb@Ka(d?Uh|cKO2Aaa*_?uC)f_9&C4bED{PHjLmzO; zCBXCX!0f?l&Q#64>-Zv-eRYGrS_XZHJ5%Ap-9>N6{$-0D;cq%~>XowQ3-;R77~4Qp zK5wJ)!Kh%cVQkoNRAc_TD@F=cbHV5F37wFQzZo5pgJp#Q5X`QkK9EUErf;jeitdGh z$`rpgl~g0S0brY6srzb<|8H#J3S!^}({y)R-iYyJ6(EHO^V1lr6Dj2Pea2zKQIRIzQ2aMBnn6&f2RoTL@&7udm|=gL7xncg^p^K zz-9DgvS7`t8-sj$g?E`rIsLNK$D7rcDN7S}VE$r^YxRSy$z}Uoc;95qdPzAC1Tbf* zecj&g1f{^M=GR>CZk)+ypSNCD$@Nat)R?pzIk5`OzX8+{i*Xx92Hlfy+BJ*ctWu_(xgM z@qDbMU7)eycyjCn1oHB-0+?ckdgdUC-YV+5n!_C#;f9&6z9pZ#bWu}OvW9V@2F2%G zGRJ|~=TS3*iS^RgC@degKrTKg3kAmfeOI>)2)y1x=4)POrkZGaUNUqavGJ|ns<3?8 zHgMu@MM#8V_%wNHYjW`Bo=dw>G-j*tvM2a@!Nc0yWA64)&a2kQ^yvM*NoL_P9dogH zS&emz3!_a3f1b_yE+E(LJ=>Z{god5cREF=H$J<3WWtju=T80g`Lh1wR>OrHsim;Ww z@?!t7!b+V>xWWT2mhP9z(V*IcU(dCJK70DoDpJZSu7rR7%B-X#kI zPzj0S8r;KnH`?-JfB9N#f_K0V0Z>r62i0|YKCEW7>4gc~7Ey=5?sN43e=)GnM6s3C zx7%az_%X~jWLLhwIc~{d>dE?wIF`|Saen+-0VL*yMOs}gPT#t|svLDTYc|_sq;9dJ z)sw-X*c(hWNufexm!?-+Cw;>rAQmr}Cxh<1$O@!Wu9I&AboT2mMW6VV z#hvZZdjJtOFJYWP8|sqv@No6y^Hv;^4g$CmRWj%iFS#>seZC2LJ~6G*)TEUkH4f@K z6AkNFX!Wxv74U0pDg?x@LRFXpPWDH(tP|Gk%FtDntSitihvPT z;>*_)w@u8%u`#2%+_K%3z^Q`<6F$fXJ9t5^!Q{HRDbxVGoSrr7gg1X$m%2qq;%-WI zw8k2MNDpMzpS^v4cY%^ij-M}qQ%eH?N)$ttO4Y0#Iv6lI4NrAg#mF(NhWG=ze&*N5 zSvOk9Le1-gCt9l1Scye$p=3zrE)=zal`Xizs&T6s@kv4(JGY@j4MMR+kC^V==H|!1 zIs5cK^l#QIzAI69{%7@Fj~5ng-O*Z>u32_tlCr)Hd_*z4d2Q+gzc^!KmSM!jCCZOI zYe<<0zcKN7O+5o@W2WZ({&rQPSsbO8N6|Ux(Nj8Ra z2_9_~HSDUueq;RGT!sO_ReYjm@t99+xBRY*(4@DsyPvE+d# zNh*`t8S{Gen+m^xYB}|OWu9QeD(n-Z3E$EHf-&H9Oa({p+Llp5IQE&jZljW$q%-z7ml2g)Yp7B<|3YsXOtDxBHT6%SkLpy$E}|c(2wH z7lag#hMJg3lqFIC4Ss5htE@h zWxx;3U%9(DCO?yrL1=p$Hf{`fb5U#&ouQvOkFdHtp7+SxjAZ&n%^+?-W=&dGAvH)% zvm4J}kJ3o33V7~T(;$&21sI!|ZhG8RJN~|Z&8D3{-4uDj1H+%b2f+$i-h!8%wFJWZ zI^=Z=aV)256XCrlIGeAcXfjd;Us$a5A%?7HY^n}yf-23$88UyC_tj8?p{l`e1{6ne z0{AMp!!Dyk9A8C5n@!kfbS&0-(rZJa=?@{pAt)8l3$ZG&2Re;d9q|_LMQyXhk#T+g z8ML)Y+G*(JQP0qu=GhgULFZ>IM&8956_zNX?#jQBPC>~&zgS;&%fEV>Qpi5!0<4oB zW@yrO`@ab-$rmpR6s^fWsQN*h*?KkCSdjwJNx>O-C;M_7_1X`7tJT=382QsP9)c8a z74hrt3vE>2`70d6vxW0g;UlfH2Gg6r?07C_I~Ej(2gz9t^%)!uIVO0f$lMdH@%Tv4 zCpKZ$;Cl73Z#&;+bzUZ|@YC~}#yzh;pE+qlCgHno(+>{MMOuJAf5V)fS2zF&&MEJ4 zl00w_bSG*rKu~L*g6gszU%h@{D z?}$C4{~n|ghb>)nNag4L;3lrjwY{qPZ?^a^O6HCobb|_`)}zS#z}>4E-+y3v{0jO! zV{hEA;+ztFs8Ek8;8A3d&~>+H8HotPf6iS4H{riKU1IlX4ZC0B`AP0B)36+|+jk%@ zPLE_IDe=03e4M+-^uhNd*~py|@~`npKa1PD@;}WqJxjgmWpb#5zYe9Y^cbS1pE&_% zkHoRAaO)%1ETh$}yaaR9mHM8ps-=ZkYS6}wR-C_J#W`BtbeK3ANS0PSg*L6j=E@2d?1=j{31d*5fyJ)od{kgH!+zote7m3G6>lN9?jSlBYXWk-M|xNK{#N~{H8&X z;cXA|u-a{-SII%u(>Ta=a?-ik7G`skk$&^?aVW_x1sxu19@kcZOEsL!&=&og49w_Z z_q=M|Sv27TR_qqEp}h{|PA~Gk_F1zjM=;;G?6iSJ2Vm$*LF(@oY-<}|bjR+v&id^) z3ZWU}ESL|*Q^0#b(T`u&F9M_)ekI5-cZpx(VPzg0g*O0gbGPM-$c^SlaOEOlPMPo2 zAZq3S_+6H5$Ul=%@5w<1EzedOBKLG%OhYSIfK3M*C+oGg#6xaJ&e#nch<|i8 zG}@(Whw)C_tlS~&GE6M0xqAU2tC$DP4|E`}hVI=!9R`t0_RDt7bNbga=Ie%`@o=D4 znt9=yD;*7ngmNDtLKLU?7gG4E**echir1G);lxw+IS48zEv{$H&0Ax&g-CGUK+#23 z<{*`@jiq9GvT|6R>d9QT>V9iVSZ5B!{0OFvNfm-4Q$?X0{SEB2d<&d@5YZS|&lA#f zedD`MEI}F@(;(`|ed;FK;qU>hD9UKbJ||vbG%vrC)4~L^r6`*NveauWD28~Lg5!mX zy%6fkWPCb=_H8TMAPvr-AVg{|SctaSgtCuGzi0*cy8rK&ni=YyoCKeQrSu7Ee6Vm4 zBfG5Z`Ha|Iv1LMh6t82{H;KH=_CXAc_XrId*oVfqDso;SL=A5{5dE=8*^)*=zSafLgZR2VywDc@-i8H|WP+|WYqwN8!j*a@=QYo*Ai=GlM?Mi`OM#yLjU9GoyQ=4qp23)XE3 z%%&7?NJpHHUR0t!J_pHUi%>?xMh-oE-);6i?N;eyT?+O~nLGj3Pv|&07-=3qF-A#l zO;v71WiluN|ChC`vsyje(1i9bB@@ok+ie|G$Xx@imYJq4>arT)KnDD~)yqpL5KeC{ zW`7GkMbHGAA3}=F&VMCzhm~nbmctkA@M~OgBIM)u)OIwrZwcCbwNu-yYuxN4Ag+0J z8#n7`w90g%&JydWgiw>TmmIhab;tp8MDfYg=yjjS9=Q?$yQ|jxXJxkFi=wX!|Du7g z3vBDtM@!K&D5Rtq6n7k~ialXaDLPm+7iM5(6#cuh9>q+B!X%W6Gj~Aoir>`$QkiFp zNV3$gjSgwD?EZLpqsIBDP@zg@=>lfcck6mXzvId+BYszCh|7rR{_O(k)vmxvG_~2M zqz;EQ;xW~+(3<9D{a97euV&kZ=mG7Xt!o>?Nj6;z8O`C{ezxFGy`WbtmBl?@9o!=X zc^cAEHeK6Mt-|n2YJ?>|iJcah8egNr)Qj|LQyjh9d`htm>EO-`WP#d~=(Y*#BwiY1 z9Z_AxoHaqOT-&%2fFLX8+n@_@*GXgOt)tkE!AN)rt@ksgT^Y)mPHG%H+SXDm#$BXL zI4eAd4B25CLk_s%4mKNpwWR;qB~=+!y0KZ4A2OSRBK>#nT(gtVWj8U z;?!(xw#RQ!{b)zA)b{$~klhnkMCX{n%aj^O3Dw{bXr?rPCC+ajH`vq;KcLUl7{D&^ z`sbtlZ?>nN8R||jaQ6A++=!GOu&6D$;g$+jBcp)Z9C6$yl(IUSb)Zh1hZJuksM*R#f&UYjdYNQ+&b}^){bFcGFl%h(NP)DCE0?wgeOfjVL&LQXul0GhePf* zFhpP?h(A9%sXYQi%S;SLZoe&z*q%)?5`DBSVA%3Xr<*Q&i z=zYGK(a%^xB3mXI2e)R^H9pCo4wLC%=-GRtGPMT?s@~1 zH1yRm`gHdi%ZnN!N~rAc3Nge6O1}$eLvO!`2F@wR0n`nVaOI7*ZAM4L83PU*@Q1IJ zzq!29grDB@)|k9hh_Jy*jj#rIOXLSn{CQ53@S4^W{1-{?&{vY$Ip%Bk3@GH-i+C9r z__gH+;@#l&tlKTD(ECz@DRoh#+kg_B%8)QNCinzSlnPWLGqXie3EJV3fm#HcFdafD z#_SyPOz~a^XqXFow$`X5E@3(TjM*{C5nk#vWQEb;MC}8U>nM6HB^n{CP`uop;gM{8 z)t&-|K$C8?ny~Gr+cmdf657?kGn(Vv+u#IP9sH{+-x0qTK>JQhH0$Vg5yIeI77u-Y z%eYhM;?m1tfLzVR8b5~`$ku4yLboX`s_}Vo&QjbVKUW7>Le_BFI}NUY*;vseOPODx z^Rwl5995xr$C+!Y!uR;g^wApoc&=!9d*EcMx5Hd@4va2NHZ%w4aGhHH;yv5_^Pq3M2o*5xZOlL{-M z%6^#7{TeD91=EP$o1S^Ag_gizHZI0 zkhVs1$7;j0C%4V6Hi&T}(IyjeHXbKOE{G$D7+-oH&Tr(TJtPOj<6^eySU;QH=@$CMNXZS z?iR|5rDs$yZbUUp`#JE?)j-=&$DA@AoEWh*Ca+michR}#rz?SvvOp@T<`X35;nsUY zV!q2+bonFk6}zW#UrLuuhJNSpv#tJxW`%5aCQli(7-m)dJ^t4@Edpt`H6T5tZNPn_ zgxP>V(%SBcfmJzyp|x^c-WSF6I6cFcH7dtI@cz2!zms@Pd25%LiNv1W4((vWG$O=Y z$=hVow7bIT`aG~!*UMB6xBV?B;haII&vYz!L#Q!1x$MjVzIFpWqr55AM&G`n4TH!a zW^{yu`5tzkKY#@dG?n=PIKwa8O=Qc3#o4 zm_-SQ$8OYNS20Ibg!IDjc`}OS=}^7-**4(}>%<22!Kl}P`sUD1gjBQdl1J;DBfh;e zN=*~9atkW_FxjJ;GS-a&Z;Ge!1m`KHZ^QRMkghFyQo&@Fl?JbE z&bAYj=K8W`HY3)atd~43mzJKx7R(TXKab#GAM82Z#rAKIdu@p1LX-2(cd24lY8qkx z5vaVVriM+va)6FDrIsoTgt{v1ab`6&>Avh6Jv^YxaqnV{m^W64iJiUft;GB)2qgSP z){YACrZ>2dWNBx5^8(R~wp^4I-L@Ge=5+$tiNZiUu{3aAh9%W9qPo9ogsW{D*CsaY zYo((VW*)2Cl{HOM16lpy`(ema`Jx3=-$|JgsS$FjL&DM|I=rm$k1gKa`R&{|D_7+! z>#bAo55#C7&gTWIX!jhsH#F-bL&-Q*r(cbKaXDs_AA)4Eqfhl!b3TD)Kw-bX;5B=0tSZfw4h8&*{q+exS1 z=s37qG_e)h_G9kyisAqi#MaCpE6RwASI1Hp|Jt9YP2LX^LBQyB7`$FJr7co1s4c$+ zhMgQIl6PSWzPh^|a2UoAq$WNPeNuW*X1gb;Y$sO`!WvIhDmj+#OF4-p$CvwZoY_(z zk%~XZYnKKzH8*G-9>sifgWjL&qb~3dR>widgYPr3@b&Hl47qC*^t#Smre?1L8((ai zTcXF^z7lI;XZZW<`RYRD_A_6qzr#&&=fB|n~QY2ndj9??o$aD7$zwsc zH!2wHD@z~&qPQGvTU7GKOB9J-tLKj49`yG>43cSV73rQ!vhM*o#LEP3$sNXTjJLVP zh8us2SzoPeUFe{4s?l|IuT-UbL-Uk9Jmo8tB*VdTtl*kw3;nr$b-UJ!U`0UTG4|Jx z--|R{V<-NM-{ksKm$GIx95U2>j%`0nz5lN%`e`tORW;)=YWe}))m>|I#@cE!c&l$i zminQmrz+krIX3b>`2j0GC0;*S#EgU^td4{G(|^)J?i;~bDB3T`h4h4pLHt73`^b() z^I23@)g~+XSj6T6Y@YVDc_q_mJxOLkZSZ7Q14wK|t~S3tPOjpYp@&Nv^#!ko+qAn! zy+?)<^?jhp%_=r1#uLx15ws!eG_;hPn7mRKxux{&K__|t&RdH|mIEy9Dw`Wb7l&=* z^S^y7o%&ryf9Zsx#d-!>RucC3IxLSRqo-HT2_GjHTL)l6LuX9Yso>3(7K|E3TG0cz z{ZiV;320${@UT{fKOG8>wu#zt+XjY&@`5NK;hJJoV-}?hx^WacE6!?m8V{WQ4x{k| zQ=8!d)rp~)_`P3ZI!IVB*95hMB9>{Gx7$97JiLe#~QJ3?0s=hrs|v5_YA zxm-gM?i{~g1xsJ97y6EcWx$R2{8yjq2cv+8tfD4~dejvWVzEBAbe3^PF-|? zS;XQRYu3qqIuSuN7H^YdPif-pGd^JJ!Qvgh^K*zhERx7Dmxi*(JUx%^tm5mm{uW(& zM~(-3@OJ;s1Ny6^PRBP_Os0E8FC>(S?=-!Cc#!+5IFmo7%9x4nZ#&KZ_g@-<_%KX^991k-Td&8C0QBBp%v?`L#9bi!W z+zS}1{(f%{1{eQ`>1Rc6YK4FEq11@=BHPkk%lnAF ze)N_1KC`-|>x>WwXBY&7VrkU8ML1M<_%eONVowz(2PAEHKQ{fNvzH{fh3ypsz1Tr5 zjJc1p!o+5gZiL3tYgJc?H@M@3*(UzJTU?(3@N(zLdRS)TNIHG@x70b+@SXpL|Nc!e z{+rzUUJ&xX1+Dh)8~M*wKK~z0>Tmx3{?}vw195#W8ze3$x+E?rnw>N8T_n2rge}*6 zNBI6B`lR;ji&I12>yGYp;j7P!0lpt6w!ZviN5lLJB#h6$diSC@`ynoUdNb$m{QQ{N z0da}!OCE_*gcflpCp!VoqluR<6M5yJ;KSQ z-z=qc#tZcVYgN71r^KzD<^EKoehK~7UR>=|bcNDiJ~u?)aQj~BI)}(hx{GLoRempu z`md|W?~npnw~RJ&GJWei_L{!qD_ZvsY0!#1-q@@E50Kw}&c7f(h~}fLEZ75UTJLxT zzw`=sBc?#UQ59M2wkOcT>(f8=O1l?(=^?|)dAYuEp$0;ceUyc!*{gqYD;?L>1F=(g zhl&q$rK+C^eo4w(WIai5k3(sM%i!_UuO0jweB1IK&p8(qq)-rfVyUAcF~rT|d!TP$ zbk#)N?N-{m+|}qNyhYKZ2DEaZH#9zAoMi+iCTF{YJfP$~hMnQ#%=z`$Z|C`iua6!L z|GL<(Elf*?~%Dfdz%Wb=bWtC|BUG?djEszbCgwlQ$xc|#52b*FG%8d-y9EWPjb(*uvhL zi3VWQl@gOgi=}D?zvJh{Gj-)k##qO^@8VnBmVv*W)W_v^Ta)<|NP~3F;A+oSwqe#_ zl56p?%&b5?1aaP%Ew1rE2%%vBO(1^hsv`1$o^}aK#UO8jCalUyGmqSiQRqH9Z1vPK zG`u6cAi&)l5zBf1L`M!)WN%eaLK1}~(&G3$XQK_zmgP8y3_)YDbQO|9LgaL;U%{P4 z-Fn|PwwDkKAbGgcD0Nj&+F&OKp^9Mt??lP5MGxcTplY{FaM67?vrS#hHonROMAAD$77h+?E?e3s(NVp9|OX z_bCT&qS`jwl>!|Mq@bPVQQMKW!HMzYRBT|4+kmA1%Zcl>^|)tNPmw)ID%sth;7ot9 zZA(U0)KnpL=^e2|rj`Zod@|c8gP|95B1J2HfH(ViA0LSl7v+iTirANfJql7fFmsAd zN^D+s+VUcygsnePKjPBr6$fr?wLl356AZF9zXOH^7@ddXTWteZN4A^`*vZ(Rmt6l@ z-@C%Od1!IDn!uOJB2HbCxs(;(8v@VM7^HrZq(>l) z?gqS!#TF_&_2#6mcYH`vX?S=02o`q@^r=xy%1*Us#gnzO>o{a$REdQ`?dJ_-4wlg&*k&fi_s^jLx1Y)<{UMSsGLSRZg|KAnxK%R}ABO8p$6SG(Bp zgBO&YnRc29FP`tI8sKI4%YD`#?}V$!Y>5D9wa!;ZC*b|ivihS}GPezl!E9m5AeC+v};72JSK;bs@yJfrgBs$~LesF1W^|YiKn%%YIc<@eg=?yvmODg0G;KXfW5<}D-Gr3xwIM*K~Z;@KoR=VC6 zCt0g~6plO+NWR$J@BAc}+1vXC65i_Pj(jS5^L^_!px{)Ow?3=9$7>On-9&BY%6e z|K>&llg{mN;KeGcgaE8h_BT_}yZVWF%gBO#Eja^sc$Vx<;ug3dPA_{g;?=_IV)sGu z+CtOlSf}ffG2hM~Fbj0DA_S9XvOG_sQ}wchj#nwhmqhyTe!;+x6r>INm{eR9xSmlH^M14mJ2E2`?2kRC*tE_N8lmq|Z`BQWK>|yK z=Wa%H-@~ga`9o3+NcJAg_SESPe{+fVQYBNBrfN2*cK%H}q25@D1TDu^j4 z3PoEJQ1z0g&m1LDeMv?=-G^~0gxN<)Ql8B1!iF1oM}(7}a(1h?>C~k)(RjTK6K)iv zgK3!i8DOwv9(-&@o*+`eR8^)XdCWNJ`6-dauU&_$^6XdU@aTs(xMATu$7g@giwhq# z#ruFATuwnv&Gh6JRUQs%D*JI}YzwwmpXiW`m443_e2w24+0k(f0viADZJz$PEx`D- z;+8PD@;2&234A%xjm0W!9n!#B4gWKe)hZ!|AJ5VsW6i~h1Sv{%Rfc{`RX|Wn;#q?t z8QXNaHN@8f8aMu6uBFis7S^UH3e)F68*OUCqQw-X6f;KS;&RP&|3)2G-_OAg;?_os zjW9X+47;+a*ecbEYE?Wua4zNM2_*lImA5Rh9oV}_lLorS*w^04npqj}35cAb3Y3Y`0eG0g7a$q7p$BnIw)BDBwD9qYtIPG4b%n5`pUd)EEp&CV1nYar)30L-|sk={l^RTXyp|UeEu9$pBtQmTW-c+ zBWo9QKNrI>{o$D$M})%>N()(w5xi(+hmVjlu2S3i!Cr479`o*fhJJ85{OWgke`h+F zxHffucM|M)7EoXIC^;DF(w^7jgx~y=;%Wmq^r}?N-Bj_YiA&NIfBJS<#?`El7<^-j zL8#JLTVu-6{D9bRNgTWASXYPWBaN%O1NjXvZy+>YQP0(Lua6|rLI}Psjczu+;eEuB zT|M>U4d8WS(E6v)0-!!oa3cY$zg41`zc63F+JHC^jo1`;LvOzhgTaHoMk%(2d1m@* zD!;<-)u!(QA*^cK61go50P6}nvccjmTKe2DWZ-x~rMuTLjj&w$L(dV_y<2(^*E41C z&9&Kf+jTIN5h&*{bi1)HPb<5r6|E#_GZU6`-4wn9lh2YM%Q(@?ctBSnwgqjafof5D zvAvx|&bT3pO>Lchm6aOJZF%PaCly^HooA1>ly^kAxoh*-QXF75vW%WCU-1Hy=&@fN zxhy!q*y?T-NxORDSalOpSJ-SuprcypuJ%EK%N+)|cwcjp#Y@lH9*(!@cUH~mu4#pB zQvk~D2q+_YM+BB(t~uA^6fHTS{W3-ycP#%( zS6o~>%d-S)JN+@?k`0?)rsx+cY%po3OvI&22w7k{F{q zU9j&D;P~%91Z*y%dJ~TnmjCkvk*C*C^-DGXH%IWV^c9h$8Q8iteOb*@{ID^J%YWKm KXt2Eb*Z%S#F#v>yMMp?e$KhheeQFe^ZfB#Xa9jKYh^9#`(2;)nclDO zdSGp7CM|JTLPSJF`tqeeY(zwMd5ehb1c-|Yw=lw^o(uo%fZ3Rx6RGG^q6v<`MC7Ni?VlZFZ`CLfk*nsH|2TUi3c?^UgIqZnt@RD?jGXLm zX<ZCRYkeUi_bd3e_M^Qw*Gm&>mlT+G|d)gA}K58s$P z-wVwP9T2Vm1>-KEx%YFSHS%!6F*cUMbn*+T#BPy*5XZX#$kD6oPZ!+$Dr>nDlz1|H zYe^s{ys$&3+N4B8+&{KHa_5ra%u_{AZLSD^O?Zxb3daWUNKh0J`EY-)aI_-FPoQ^* zh&)8Q|L?ma?pWKx3)QXMzs6{DIl+Z@+yGyi+^7hMvb{r!!!hTS@UPs!ns zz24hXBiy4m^;6H^hxvZ|75(>NL-(%&uK)PPTeSj7NHBxq4rRsXME`v)!{u@Nd-@=_gKp6}n=!u%8m@3+R%d3*m&Vh; z$=B&zXVq-=mVPx=!~uvAuXI@tycM^~;h@^J1d&E**9ClN)CMWC{eee}TQFebOJ)0j zDwy)j?Z=32)NwBZnh#fOnypP7VM6*$;{}U|!QJcUPis*#`&8cijE++w^LgD= zWThP^7o4nKP}mHX!I>Z94TF=8+{Qvqjm~SC-rupx#@%x}Fz9b;h0{2}B`kQ3uhW54 zM*q3hub8WAgv=k)xp5o~;&!@Cm72-~2~H&^TbW7;o<#SxRo1ongIrSX&ezN%#u_Un zyn|Zo5RkG=aFXoQan%-AhTahew{w8h@F`4gNGDjrM+f!yoO3S#Hf+Zi1Z&u(F~3}D z2LJgrQsfL4ch717Qv36P&EGbcdgmgR=>+O1vtt$k*U=b;4C-Kh{urwiKd#e@YESF$ zx_qxcF!`#VACykY1rCz=L9(B{S+(ACQLLxF`}PNgJ+ljyCL7{wlPG>E(??KTr@ z$Hb|li#SfsY64dD>Q|I~H-2AgxZP+pr8an$fx@d{E^MNlftBsElQr>w>qa#Ba}xEqyg zUu91n$m);&ZWe?zsGANW3*s22e;a1;MU|cu{G>01Q@))mv@jo65Tr!*5D;|jq~}vodxY2htF9{x9y=7=Tgl>;E{lh z8Sk9Uf(Wi=d5bb6dILr6fC2mIH-es*l%Q!31*;~aq`v%mtY}U5g(VHHOT=r{`Jv3! z=)!R8IMc1Rg=)7=_$qa7zIOD+F^%NLKG?v8tMN4l-Rj2|_FejM(XGGfO}X1GCP^R` z7i?2FxWQv^4Coe%Tk(jqv2<}F!xmvt29)q_R#Dx|(P0wkKADxD_7t}rds9a6sA_&J zT&c|~X>VKc?zx4=@q;hBK)`}6Iwt>i+?Td8G@pjpi!0mt*>Vdm_d2xg zcCdKKu$7yvV7VEbe08)GMywP36#v-hskXt^f(f%kOCP<}-AvSW2t*o?9b^STA#Q=F z@yhnfK^Zgn-=42)LbMG~{>C=$W7b%e{Ju2&Qn!C4t zGqGb(eN}q+`{LO&6#huIAI)Q{a(RH;-0oZkD)6i;6%KB3!Mc+KZx3zjG5XubN|(xs z)_@NnC?&LWs+bZ@aT}N4e^z*zvf=Rx{C!Gwm~w&GxR1bDitkbgNEo;BY`BE94n!!$ zySkK3hZ@3XyjL~$bPF1@-R*h@#hA9Vo0$j=>N`(OJ zt4t2rs&uZl0eA6D{|~`lM>_OADZy9qcB40~S za(2E|yM*1r5cmH5INiQlS7et+SRwgCeoe$~o#&6k{!9RQiTv(5CvAiJfZ5-3=i&hC zn~2EPiuhj#h`Nd#HA8()cNf+5_`F9X^uOL*eRLk6mY_1yAEMm);0dHl9ganR1)|%|2vfbu}QewP~-|M{>gW zH1%9K@FN1GiQe%6WCif^LF}$!!=M_;PxFmUR9>q%OZnvv!zP+q56c$-RYffqM{29q zCp`=WXTvun>qp(|gEs{E}=0agLC24FZ^)U+& z@$C#^wj zY(0JbK@1Px*L;=wee6rsN?>?yoQ3dKhVHJ3;9diC>&_b3xBE+I(MpJsUn<iGhIs_-uv3E!7SsMd&F~bZ;{{A-ADTPGT!xesY_=zLc{k_xECEv<8>b@z#nzu zKOSd8IWcS2P}pe=lQVLi(|#%EkNIS`uiJ4PSSm1WnasOy;>T_(rbmbsG16uLJQ8a; zdU`)}eaN>VqBN#TVt=ul(GhRevV8UjbBC76X#DCr&mJG^6F;?SIYRT%YFn_2?V~;b z;Id?&%*07j7p{k3!isB^^&x2KjA zSzB=MTBtB{|1olDZ(=TWs%EfyN5S6ij%YV~{6LuhK(PY8gEvS`KLVY3@Bk3;OLF;3 zs8)YRPL#*5{)Oj{QE-Rk2iZWHcX*=X?a1Hhl2^2&EC~C~Y%O#Imm9th{k2Es!62&b zMO5?8=nlG7Us7gTXndB>;HJhs&dO@*o%#cFkNg zX||^2-B0rQPJ8YB+8$}4gb%|vac{-~B^6N^^Yxa4Luo`t;Vi=6IIr9-Z+axgAu3oy zDbBRxVQuiwNlDpF&4umUOrh2LN}Z%%#JfkZJ{@|eVnl>jKkAl*zT1B^1)!}cpL)RV z?Q95rYnACtg!;kM{YTr+GvQ=SbO17Q{E($(xoy1xe%#DM93DZFPNjny97NmNy&dAq z_&EcMAdmXJ-V`J31}lG~EBXlV8M!#Ne?9isQW%ENJ+rXLG1t$=o3}9kD27oE_5`vW z&)5j-vPZ|!wKYLn{OaMzHk6C|rI>RasN&S~lup&0y|8|av_j=j`|q#ZBlI*tCi1WQ z7Gml#v;~R%*C=t$+5O3li!UD9GW5^~;eQ&#F;{Z6%$W;kQzNv_E8g;kETn0rB|0v$ z?pB_n)TmiUmPDD<=f&~7MrJ5jdS1V`#@ZB;zg-y!dG|ApL` zxhZ9(t|*0saj|ab?9v}-Bbc_o-LscG5K^q>=X)K5Nc-qM$9Hf=&E&NQt=Q{d6pr+C z&7jxl!+~!WN9=M?lc8KvwsZEU=l|(H?b_cH2tPU9c?At_JfJzkeII}g)3B_U@Jve> zhsb&<{nWEg)@qFLF5%|m6C9({hld8WhdB2ze$)k?$I+6$+6k-_+~lctr>VYvsv@`T0;WYbeGZ`D%q_XthC_AlrFjpVn6*uME@ z1u7aXtB|TDyVtwJ53aTH<(kXBU;22yQl}gD)hcJhJpSwecin-DDTpZ^>hZW$Lau>@ zJeu-QMmw>n>)pAx9M+aDuC7cUp>`tGsn>*Y zZ82Ktc*TT?e|@tHpVQhs2(CLcV%*%ap5WM^h{xk+W{BOX)(bNG;KFp}gS)&d&_Ffa z^wF%B#zTZb-@MhJNl$X7ht=n=?sl7ucb4Mx(Z-2s$=vaM2Wd^b(6tcdo?@&&#$DcX z#;K_#K2llpUJbl4kN?O$%LQ2f8S4_?q{Q!m`9W(O?1j&INWPY-Gh`Xp;(kf@TTNKw z=9dX)0Pw~T0`5Vh2#t4=xb4Bs0Mm1=ItZz=}R>Uy_w1j<+cR~%umN9HeTO6T;_$z8RgmjA{h4TBH;T@5u=!U+c8%z zQB&m6E3;eM5mEb}&Hv2FKL+v7)9{Zg_{Tl||0^~;OiMdB{Ng{tAjbLxngkNou=md@ zh_p@^-(4umhLkLxqRUOl3CmJpfL$8aJNfk2{|KV7deV|!54MA@)UA)tB}Lpojn*TL zmFNFmuimN5g1#^ZG{gU%+1PL?Nv|!W?%a=4Lg0+rrB7-FH|j<1z-=?Evtkm5jtA4W z%i>6Kv|0;i#CiC%$X7_#IS1Ti+*^Z7pq-JH!l2z8Mz_%xA;LsO+ydSzTmqe)lim*I zi{TNmgPhCH8xuAR^y~&|4nV;Lx6u!Kyo=_?qUHH#kw#qL9@yiS68() zVCvCmDPXcOcgd!~iFARq2FM<L<_>5Q7I(74hVji&X@FMx=frY2@ORVZ#sMnuI``bCZVR1D>1D$ z56fvqg^;IB!U!+(rdaG5SZVWOa;a8d@>{X4{*cY)TJ50Gm`iL&(V7E8_TFmv&3QUS zL@e7~bmI~TT@KchJK>#;uYGSYI}-7w%G~7rA^jAy^9`#vV>cgU26Mby4DBcORB3Dp4CdfEjWzxL!CzUu8dW1 ztYy8g#W0ThbklY!Stlqi90yv=+Bxtc{>FWoylnmU3?q#-z^0D;l&!C^hHh|* zpH_d;r$tEfOdvptc!cWj6C>u`SCh{i+ zRX(y_L8(L4vAzOK37whbYiM|5g>&piIf-ZWV@65a% zpN{#BQmo_FJmMTkPK};ybTy~ZI{??qOW&L78yAFAHVw!T1EEDUJKI*;SB_i#+LSrE zY-{Y%tj>|6f#t{!G-P7jvkbKe_^`$9pI-?f zPQhrvBM7HtW>3=ELfr%p)cj7#TXn&o2`m_%_*4yG-Pl5ATyYAjb=OzOW^S^^t86yK zbz;f1eDi{EPTT|sREi)Mk-)ki=}IW3>+Z^9gzudMS|5S-dS7;6F|VW*cAMz--Txzo z^R(`6-x+CiWqIu&^JRaw?NKrg1&t8q2V#SBBx%sY9Q zw_-i!BFV0P%e*%kbB7*uGg9S9U60rP@i_nKusqawRnFm__hKhC5pi1)iDp>s^3oA_ zqo#g4-H>6qA2VCrT&#C0pIC2Na(g7Z6!+!yYn}R^^iw(2Khd;Fnl!M>12$NHe^zSR zoJ>8&!?|qF^09ebV7O+VCqpb%w_q1RQ@2g5yeGCd zzB=GFVM^cNApX3MGwDjVzP767>*Ck_C;9~KMN{Wb@*E<0w?@7;h2foUn`2m zNK8CWX8xcBCh!)2C1# z0zwV96rSK;+T?ZGeQ_BX-qrKLUElv1JWKm+(i34DXa3a$uyYm%+&bG>ZGU!i+ zgR}WZGggB7BYihstu`LK3;1p1LI8-P(M_IJcX5;$oDPBi$_PI}Q0j92chSFU0!I4sshI4Ilb;YeS8%HKf=!yd_vN;r23%{W_ zQU$?RdgjsE7zYx^9%M5j&6g%hRd4!qk#Uxv`s#GUfEA!hhuMjkQ>Br> zaj#K*mzvS&{yw)Y9SzR*WXvzytR1%zoZ$xH;m;~{EiEjQ*4DNEY+i^bk(}i3T$Af@ zzgdSLt{tx8pAFZUxP8WtmJvRj^U=eqKg`e>9)Nf?KrDQU%(cifk#ozwEs^ThFb}Wx zP1YTyQxbV-j%AP)yeGMbK$`Fz(MelZscFicq25)~d}=U%tO&O@Zz{Bq`EG|daanY0 z{Bdnv1qGAux$%hW&g1c3`pGMU&{r{)F@|sZ{hquw2tzsYsy>hz#gWEa7E5H)8Ys~+ zHrPjbW!_l;9N1VSd+Hma1CnP5o%ogG+8;Z4r$>X0pK6jLUz@m8N17(bS|)`x%Nx#7 ziabC!x#%L$^BVPGIP;o7e{wjCj4K0qADLN;t-OJB9Ubkh2w2r;hpUtG^B>kDliuCr zEg$IiKq&^_+3O8BGt(%Kfx+6n{TI^B@2$P8yd>=vvk)7dZWS9CLcU0ADZFK6?^R~t z;vBNRKQX?XnVUU!NK4s@Hv17F>sPVSr!8!%+hV{v3$dkhN*Oz7Xz4suKD(p@Run4j zl|yNTl{8sI`~D=B=V_PUd>wP$7E_-o={*{+G|HazjXpjRy$~Th=GJN|`?-9V*JP`F zmXBYz-cj^Hnrf#vZZc3aImxgxF=4z4)?*WFPE#+EZxFZ?fW&H`1>>LHvt;g0?sdeE zAY3zGJt?sEfp>NaJ5TjHs^gq%v@$=rvj@&e$zi+{myHe%Fk%4nKtkDu<8Z!x-gyU% zr+fge_P}huvsOK!lZMuN#+!qWtSi+ps%jm9=GyZfXcTe1eP#+TIXZCgrkH5WF?Qy; z@#ET2GEx)aTf{{cfu$sj7kW zyd7Pfea`7+K0#EIU6qV$It*pyJgX8$kqY05!~{p}N|L&AUs6ZdC$_IuTZOwkqpbcD z0AXT@gY^xLtO49yPXk<~Lrq=xzK>y$6CJ~l#J-N=F0Ye2#~yLqvYUVN%(-Y)#JhyV z7pWSTNZ$7+M4!^!{iLp6ou!j!-k(PNUFxw~>)_tIc+=vWkpSR`Kv5 z^HN2Ac171%jaa>>J0O*7!o0?S>zl%Uj_;3fv!3C$l_8YB9kiFXpro#j_nOr2OAqI& z%+^uQvNN-Q<=w4wl>DoW0q@x7#XqzOg&|%R)PiCErMx#deA=mz@M>bP0p|5_*Bd4i zon|iABf9zLml4;A?Tp49rgTNvOfJk=?X@RwHNV?}yQ$KXzneBhR< zwfDF}Mj149Mn@^r;;KC*uZthO=T71ufL&kjY!?H4$uslA7OewyZm!>rzA1iv{+FWo z|4=^sKM9Wii|MM#AC(k$!jC#^?`Hc~UG&~Y>ru?k38B{5sp;BufE+Vl{#O;o6A^K{ z@Sp0b!MLQ0OJU`A6<>u8MNAE~0JTJd+YM02IP0*v5-HXDR)_sz$CMNQ|_&>_X z|GLTg7$n;?+MlGlxwS?8M6s>E{jD9WDuI+KTDLRYiVyc8Z4~3{ML~5;S8cITZ*2v&&bR^CUrfdvVa_ zFDC5vN1Xym8RJv8K>SjSfNUw(!|&Fi4j>KMiwla6vZEgAU<4P%G|(I^L2*c<=GId1 z!Je1p4gR-t9S`?%cL?QNN1(78{u3ffvT}>pRa$qF+EBN^t)})!F=WoyZ5MQ5twjM{dtjBG&r%mBd zf_F_Y+**6$_$XdBKAR6W%p>E`b?WGYcYo#caCtHv-LftKB6f3&czJbrnrf{UwKMqZ zwxC6HuRF-hJ(MdBqt%zdWwJnfYDQ`JkJQzc>St9gj-<%S=*Uw_z4roTDuB}7x3w_|D8znG3d%oqJa0eMt@grw=(UycV|keVFE7rzCjHJ z-Ct=LbxR5|=O4|RZ)uP?ZN*2a;i)UDVEbzx(`uv{qZLH_@l8AetWaSOm)6>Z88P}0 z=c9B?)(nvC2D9vqrHDN>QxxIN+-iGkd|)XTrqEuD;W$%tMtPf|TYL={w7_n4zB02r zN&v1rz;-st%6f4fGjpWQ@=3V?o{nfj_{0dpj9VO~smen`Uj#M=j}c^w{=<13Q9TW9w7EE+}r)Dxkeh<|~L)-Cg&6gMz!`Khm$ zq`Iqji~1~WPVs`_YUz?JTb(+Ia}~EYO$+hVR8Aj@?&Sp2krWAD+-d#w4i&TkUPrKI zUF}mb-K!d}qm;Amc!{^tGe~1KB981D7tWj}R<)SJFA84Ab>1FL=q}2jj76KYjwWS< z@cFZ1R2vDW3x7VS6yQ{3ivfIkpwARzgU?snl?}B90PT|;MP%f+bw7okIw3}IT>Fg)4)`=6NJCf& zrxR$MpV=R7pyT2}2f&^3l?5_B;FI}wW5TS69I3H6e?0@bP(AZG4V2pr_4fa{r_#3m z701V~UkKNYBSE(qw-Bh8% zg;Ht#RH%w3ZF-Jvh9ARUebM^yhoWwVkIF+e`q~J+e)|`(lIV@aSck3KF5J1=M*7O` zkH7tLx~1xnw~EH-^lD-abge6*VSUOUJ!j5)(O6|OUnp9^d*hm2QDswOi=Y-NA$fXC^Wus4=pw9{x)CKQEMfzxIrYqFJ4DJX1UeljQ_ zen~U%xU~_`H0ut7%Xr%x-~H}Gy<+frK^r|h8sy>!&P|yQt(b5dR_zI_w@e9NM?t-O z8XgX=US8!Q;&nIuKb%{T@V0G;d%s_1hoSYh9%m0|4%O4JKqWnB91fhOaL!8I(EhWh z^}U?FfdPJYeLPr0wBeb8rx?QTbdS}_V)$?8+7_;iSt2FVdy_MBXSRr1#UJD;)1Ojj zl(joGK$Fpo2nV2%kjD5(3;M_#YmwJ9CBpNa74?M)@1Q!AGwIZDxVYKY9OgsThZxDd zwz;JEQ%}lQLv7U&k2`x+^P~qouS2URu0J|(OudixZPF&TV7My(9BthLox-yK!?j&f znJ@;Y$U|9I(woIy@n~3$cWLEmC3iHm5~f-#4IonIH3Gr-5?pabg+n_i;P>~hK*S4G{z!QwM*&~&FySGy z&P}|^mT5Leu7VEdo4_<%9A)FB8K=?8GARCasaL0Na!o=|ZDpI+> z^iX*D792>_a|a%lcaX|idhlp=F=9TBw>~D-jq2F2wCfhes`8}$tQsTkfaMCg^_{$( zTUuLXQ!hV)c$SBkq>3Sc3;SBXNlJLL5LRGol|J+$j#&*OfzEMgUg!S+-9NzOFqSgZ z>5s7FbzLk%A8lJ#aqp^t9k$9B#35Eo&r`$&t5cXg53xcR&*~?awQDXTI8^tbu-?jW zkH`&w;#yq)Wn=t8LFXDj#^tpt*0I7zMCO3db;TX%@c!A#C^Zq+2YiP;R zxEEC_Q4wMlhc>5Tds}HRnSe#SjIESc@CIw;+z=u?%)j7x@7xDfc(0)xR z!!sfFfNqOjcXplMwl%lSSMe7*mxJ*S)JD7T^f}J8;3?W7N z4%vLQOtoqUf4r?)vZY*G=#w7^XQ5Uhf;{rPDxOY(XmKT61q>akDu1xPn_YCKO#@@a zDX)wJ#cr4lcYuu*aBmp3^Wh3w{Kqa2>T%q(-wbVOsnU%Np8^scLM}{+fh%k=jCz`x;Mh=7dN0{?pEt(xpzB!PxP3tD5pCp zy^PZ6=^N=ge-%Egk}nC* zF0>8$eP+42%X*n3AaD4jMD`A*Nm4(p&nydQ!qLy4rLpI!ld&|cOcHLL7=-jOW@k%! ze<=>iBMZ5YQND5Pe1W|$Qi1Ci+PL|!$He79cNAq*RhBm34_R}JS_u-Apm`ho!7b_+ z1xrFW`6#;R>*^47$)3)^NG3=*t6km_!XRt&+1>qeJtB*zw{u>7bO-vH%EeJ5O4(Sd z{I1?l!^DigogQw z(AKo>>o%xFq$C>Qc zw50`;{7loj!|6yYvV|N)B~qp;cb%$=tQkrR znU+6I=}Nt|!o*%lxjGe@Be)`WbY0WGy109WDd9qdPA8GOX9_@y6s`R|DLB^=169`! zqu?0`)*p}>qm>O-kf*)&?I+GP`fq(aE~NhkLyQ9w9CiJ%6ye;3!&u|XPXr{%1Q$1H zHb#gHb2wujLM&}plMA0;UwoD~C`sKwT!)~4wgNO`gs5Hk4e;g?`C2@`V>vv*ktO!d zHBIj8(fvIW;o zD|{DkQ5KaS4g0VbC8D%jcqe(607Mz*+#}S@>w}5BizLZ`Of{5}6~`Y)&kuER`=tMD{uk!-&z$^aW&aq&KL+t{{@@=E z@q^R&$7}u#r2Ow{5WjeOdR_?sPw`Ie>0a+hA)day2;@#>o#jzDy(qf`e&s*0IN{O~ z{}YR2fj{T?^51RQ5B64?Ps11IKOcX;-N!u?w&QE5m%~Y6154#!@Q$+Z0Ku`eAE$i0 zy}%`Q`IFjT0M76)Pm9P$IpUK>!Y_ZrM3VOgVL5i;k|GV+Mf*myBHjMIdS$z#d_(F3 zH*d!Wd*|&{F^^()ZAWe3llX&Iks5{5pSBl#a)!%>B*(5tL<(DN|E<--ANcRWP4iZn z>d?}ry&E0eQb{PcqjdQ6o#v}TW%Ju6t2%gPZBDQf|5BaxE);)n9<3 zByMp@E!XKlko;_uWJVytOFauV=0u!lC*Rr_FB~N><~LcoCjyc(8(+np)gT)H6=&Tl z19whd*oId)?sfd3KhS7bnNbgld|&`_o6UKEl*eCW-4a(Qc!&a5|5QPKXfQQ>#OJl= z@mOP*@&pID9*_M%&O%JI*;xS5US^<0KQoIVbNCy>ekvl~x4h|~+I2y-2kw?~PaS@4 zzbAF<>afzYt?^bN^^J3xS@Aen+CbHRz+SlM_e+y8!34t z6*~9023pg!H@IHhy9%|S`@$Kz2CuF*vM0MuhOB8?8dUhX!x~>4G271(>#JV zaXK;)Iz|@v0S>~y-a>;T%VUmSflI2WXuURATa86;SO%zw^_qooIy|9g7-yBhhvhAm zo8thf79%UGVTRbp0|8-PMm?ru(G*|E^^9=Cij2NkoT;1W*VEK6ekLUG2XR9FYn#Ezoz8%A*EoyEqUdas2;XB$Y4gQ{g^RZpUmo}fn-3q=9bBJ_ z)fWOhYYn8ZoQai5GFDy)I#8}tKh_NUL~gG_VF~+9lje~|v-7HM4xOu&m=HssH^$SPb`@15lf4E&lDo_pVlSO6}pH(_5m8NS@K1* z(Qr2KG>oxf>z_>`I@dA-n8x|3>{$M)zr$s4%msSfJo}pF#tnys%`2k8+1rLQl>50? z!KZ!fu_u(y={+R5Ubti()b!gAR_DaGIaDz|%D`Pp!n=B}v9R@LS)SB878aDBE4klH zzUzdxo&mBnc>no7=UigRFk7=H=XyW7IIKO7S%{)lGa5AZL#I}KwXqDIrziN!?VBEK zOgsKxu+CUvc+Sus$LXJ|4e$Cw zx?daVIRbZhZ;gKnRSfJ1Ke&tND{)NN`SJHRB>$iz-CUk^YX~2O6=BhiB6l^w72iXV+t=Vl9eJG_9N0 z4a6{a_urjAeYh^5kl;(aC2rJW`OKk`~wE8XM)@v1w)E*lWv~OuE{E zv|fC?xKLeTAYf4jr*)_c8zG13H90D>g!p{Oe$u3bvn`CGUM^UdJrQ1CAPCKWGS_J3 zrh)T0LT`Rga3668eao(#Om3pER>@|v{mH`~(Qw@L<^W`QZ@I@>@)focn6ezLfZzOR zt~fiu(@t09FaqDfZvGzE2=qK#q|(zB!t!TfPmK_xG*4!Q65S8`6(d|Ws`q}?+}Go- zN)HmLaCMNXkxfHZv&MGkIgxHJZRC*ChoV-*Y7SU;b()5k3$<_mR#|%st7EONYZUw( zU@&jr;$v6p>f2}Se6ws4T?-(JQ7JTe6l3!+hU`~-S=mIjnOqvw+LW?kKh=~cHOTRv zYkvS3k$&?fa}ZkMgcL4tvPbp{@i#HQ9n00evN!W)hlF|p|HT>eLa$SaHaR#9_UR{X zcMZw=oxveJhvdGPvcgem8D}ihjglMzu7@?Z_GP?xU7=LeWgMFi7^}NxVn@6PM3MuC zvi!1#ZXjr3JiW_K$$jMPcfM>#f1 zCb*fE7)tR*lPhZYaLX}v^*nZcwp5)2);>7fs&Ibp8HT37#Q9AO$KF7ms3_vxaIU25 z(Lq|KMz*>kG!)@;U4BlJ#_SubeqnqJ|MQD85L>4rd&X(PEa zP>b+)H5e(^p!0QU>oF(&30^V2C(MiR{i4cU4nZaA%_EKl!d_Sh9V1Tji(4O=DOwgw zfwS8P6!mK@2N1QE z9B`f!P>vDeyQ%a-)$_(k?bp~Cp|F%iwDHNGD{%I4oGb8oUlaE_x*S_qa+f&H?Erus z;ixx0&SDK$ck3K_#t04%&?`e&lO9Ioe4bdl=~~-aY)IH%Hi)gmDb58bb)Da>W|@D# zveY0n(^be6iRv57;KZ2P^B8hI7cKn^brIe(Yd8IV;PQr9vpr8e-j=B%Kdgm0FI?}a zLL8rVxP%+%??bL3r;M*6{jerSP6)Mc80LKUR7(Nhl4*1;(i7#_B+?mWZ1T=YmJ>Vx z?0vr&SVJg-=X=C$yeVh5e|6Un!%C0qrx50P<1LJ1Ip3-Kp`-ldp&LHJ3-Ct=btJC| z6PWm$G_eOOq{|9^Xn2wF?gkae;oVR2M}O_8YlNf5EBeEDsIJU$tcSv^?x%Qk;R z&!#%nj1L=V85pz=unw6Hz(;f57y2~C(a`0X6lwDi{ud!86x08L4kq}2ja|u$)cdws zw!Uenu`Y>r|R}f4$(36IbXxjH@l+?)!)A@ZDawH_PQzIot2ZqhX_T=gc zcI~{(nryI)>C>J!sW-(;>0tI=?GSy`S}aNZcy*Xu1vDmqSn^D)d0M{@_ zqSV^_mzJf8$perXLsgK1Rta(1$osN!o_2cbeopC8dCGiJsLR-2Bo*b=`nw`kyOiU5 zrf{k3=?HGjK}s$mtBV=1u4x+nb6MeM%wwV`##1y_PH4zQVHax6PIRfi;<$d%()+2K z5Gmtxf|}YtnoqHL-LmfVAIA%b;!~) z5THQ;_MCgu*H2`wK4*fxsaDGzSnYYR>N+u=7{eg|?Z@-tLIY_4(qCB;Whvkmn?^|C zWJzlc+bY_dmIP8RPKZXxc$$}}|Mt3n=I%C@wW(sE)nRyuqd~MYMELqh&8@W#3oLKC z^m9sYbFVU)B@aJjp9So?FTCt$74>VtqxzM}Y4bC!NQUfyN=+gOz!fOhJU*2$en(Z? zNu3uKE=YHjBr=1-wBFb(#HmomOZ!}{m3|`eFjwGmTgp0LC6A(axP{QO`S0tYw|#;r zJKkZdm1dg|Y~Kk)^l_2Ym`~QS@`tjz4 wjdKa(a3;3+U$06+d7=K-yiMw0yFm8O&W0(_N|chY)D*dV!SauabM6WM0~B#7G5`Po literal 0 HcmV?d00001 From 873821eee076aa6207d137a4688dcbfbb73a5bd1 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 23 Feb 2023 13:19:57 +0200 Subject: [PATCH 146/188] Delete, as it is obsolete --- .../commands/base_import_command.py | 16 -- ...mport_destia_street_maintenance_history.py | 59 -------- ...rt_infraroad_street_maintenance_history.py | 63 -------- ...mport_kuntec_street_maintenance_history.py | 141 ------------------ .../import_yit_street_maintenance_history.py | 140 ----------------- 5 files changed, 419 deletions(-) delete mode 100644 street_maintenance/management/commands/base_import_command.py delete mode 100644 street_maintenance/management/commands/import_destia_street_maintenance_history.py delete mode 100644 street_maintenance/management/commands/import_infraroad_street_maintenance_history.py delete mode 100644 street_maintenance/management/commands/import_kuntec_street_maintenance_history.py delete mode 100644 street_maintenance/management/commands/import_yit_street_maintenance_history.py diff --git a/street_maintenance/management/commands/base_import_command.py b/street_maintenance/management/commands/base_import_command.py deleted file mode 100644 index eb102f8ed..000000000 --- a/street_maintenance/management/commands/base_import_command.py +++ /dev/null @@ -1,16 +0,0 @@ -import logging -from datetime import datetime - -from django.core.management import BaseCommand - -logger = logging.getLogger("street_maintenance") - - -class BaseImportCommand(BaseCommand): - def __init__(self): - self.start_time = datetime.now() - - def display_duration(self, provider): - end_time = datetime.now() - duration = end_time - self.start_time - logger.info(f"Imported {provider} street maintenance history in: {duration}") diff --git a/street_maintenance/management/commands/import_destia_street_maintenance_history.py b/street_maintenance/management/commands/import_destia_street_maintenance_history.py deleted file mode 100644 index cab1c05b3..000000000 --- a/street_maintenance/management/commands/import_destia_street_maintenance_history.py +++ /dev/null @@ -1,59 +0,0 @@ -import logging - -from .base_import_command import BaseImportCommand -from .constants import ( - DESTIA, - DESTIA_DEFAULT_WORKS_FETCH_SIZE, - DESTIA_DEFAULT_WORKS_HISTORY_SIZE, -) -from .utils import ( - create_maintenance_units, - create_maintenance_works, - precalculate_geometry_history, -) - -logger = logging.getLogger("street_maintenance") - - -class Command(BaseImportCommand): - def add_arguments(self, parser): - parser.add_argument( - "--fetch-size", - type=int, - nargs="+", - default=False, - help=( - "Max number of location history items to fetch per unit." - + "Default {DESTIA_DEFAULT_WORKS_FETCH_SIZE}." - ), - ) - parser.add_argument( - "--history-size", - type=int, - nargs="+", - default=False, - help=( - "History size in days." + "Default {DESTIA_DEFAULT_WORKS_HISTORY_SIZE}." - ), - ) - - def handle(self, *args, **options): - super().__init__() - if options["history_size"]: - history_size = options["history_size"][0] - else: - history_size = DESTIA_DEFAULT_WORKS_HISTORY_SIZE - - if options["fetch_size"]: - fetch_size = options["fetch_size"][0] - else: - fetch_size = DESTIA_DEFAULT_WORKS_FETCH_SIZE - create_maintenance_units(DESTIA) - num_works_created = create_maintenance_works(DESTIA, history_size, fetch_size) - if num_works_created > 0: - precalculate_geometry_history(DESTIA) - else: - logger.warning( - f"No works created for {DESTIA}, skipping geometry history population." - ) - super().display_duration(DESTIA) diff --git a/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py b/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py deleted file mode 100644 index cb4b2b2a8..000000000 --- a/street_maintenance/management/commands/import_infraroad_street_maintenance_history.py +++ /dev/null @@ -1,63 +0,0 @@ -import logging - -from .base_import_command import BaseImportCommand -from .constants import ( - INFRAROAD, - INFRAROAD_DEFAULT_WORKS_FETCH_SIZE, - INFRAROAD_DEFAULT_WORKS_HISTORY_SIZE, -) -from .utils import ( - create_maintenance_units, - create_maintenance_works, - precalculate_geometry_history, -) - -logger = logging.getLogger("street_maintenance") - - -class Command(BaseImportCommand): - def add_arguments(self, parser): - parser.add_argument( - "--fetch-size", - type=int, - nargs="+", - default=False, - help=( - "Max number of location history items to fetch per unit." - + "Default {INFRAROAD_DEFAULT_WORKS_FETCH_SIZE}." - ), - ) - parser.add_argument( - "--history-size", - type=int, - nargs="+", - default=False, - help=( - "History size in days." - + "Default {INFRAROAD_DEFAULT_WORKS_HISTORY_SIZE}." - ), - ) - - def handle(self, *args, **options): - super().__init__() - if options["history_size"]: - history_size = options["history_size"][0] - else: - history_size = INFRAROAD_DEFAULT_WORKS_HISTORY_SIZE - - if options["fetch_size"]: - fetch_size = options["fetch_size"][0] - else: - fetch_size = INFRAROAD_DEFAULT_WORKS_FETCH_SIZE - create_maintenance_units(INFRAROAD) - num_works_created = create_maintenance_works( - INFRAROAD, history_size, fetch_size - ) - if num_works_created > 0: - precalculate_geometry_history(INFRAROAD) - else: - logger.warning( - f"No works created for {INFRAROAD}, skipping geometry history population." - ) - - super().display_duration(INFRAROAD) diff --git a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py b/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py deleted file mode 100644 index afedebd22..000000000 --- a/street_maintenance/management/commands/import_kuntec_street_maintenance_history.py +++ /dev/null @@ -1,141 +0,0 @@ -import logging -from datetime import datetime, timedelta - -import polyline -import requests -from django.conf import settings -from django.contrib.gis.geos import LineString - -from street_maintenance.models import DEFAULT_SRID, MaintenanceUnit, MaintenanceWork - -# from django.core.management.base import BaseCommand -from .base_import_command import BaseImportCommand -from .constants import ( - EVENT_MAPPINGS, - KUNTEC, - KUNTEC_DEFAULT_WORKS_HISTORY_SIZE, - KUNTEC_KEY, - KUNTEC_MAX_WORKS_HISTORY_SIZE, - TIMESTAMP_FORMATS, - URLS, - WORKS, -) -from .utils import ( - create_kuntec_maintenance_units, - get_linestring_in_boundary, - get_turku_boundary, - precalculate_geometry_history, -) - -TURKU_BOUNDARY = get_turku_boundary() -logger = logging.getLogger("street_maintenance") - - -class Command(BaseImportCommand): - def add_arguments(self, parser): - parser.add_argument( - "--history-size", - type=int, - nargs="+", - default=False, - help="History size in days.", - ) - - def create_kuntec_maintenance_works(self, history_size=None): - num_created = 0 - now = datetime.now() - start = (now - timedelta(days=history_size)).strftime(TIMESTAMP_FORMATS[KUNTEC]) - end = now.strftime(TIMESTAMP_FORMATS[KUNTEC]) - ids_to_delete = list( - MaintenanceUnit.objects.filter(provider=KUNTEC).values_list("id", flat=True) - ) - for unit in MaintenanceUnit.objects.filter(provider=KUNTEC): - url = URLS[KUNTEC][WORKS].format( - key=KUNTEC_KEY, start=start, end=end, unit_id=unit.unit_id - ) - response = requests.get(url) - if response.status_code != 200: - continue - if "data" in response.json(): - for units in response.json()["data"]["units"]: - for route in units["routes"]: - events = [] - original_event_names = [] - # Routes of type 'stop' are discarded. - if route["type"] == "route": - # Check for mapped events to include as works. - for name in unit.names: - event_name = name.lower() - if event_name in EVENT_MAPPINGS: - for e in EVENT_MAPPINGS[event_name]: - # If mapping value is None, the event is not used. - if e: - events.append(e) - original_event_names.append(name) - else: - logger.warning( - f"Found unmapped event: {event_name}" - ) - # If route has mapped event(s) and contains a polyline add work. - if len(events) > 0 and "polyline" in route: - coords = polyline.decode(route["polyline"], geojson=True) - if len(coords) > 1: - geometry = LineString(coords, srid=DEFAULT_SRID) - else: - continue - # Create linestring that is inside the boundary of Turku - # and discard parts of the geometry if they are outside the boundary. - geometry = get_linestring_in_boundary( - geometry, TURKU_BOUNDARY - ) - if not geometry: - continue - timestamp = route["start"]["time"] - - obj, created = MaintenanceWork.objects.get_or_create( - timestamp=timestamp, - maintenance_unit=unit, - events=events, - original_event_names=original_event_names, - geometry=geometry, - ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) - if created: - num_created += 1 - - MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - num_works = MaintenanceWork.objects.filter( - maintenance_unit__provider=KUNTEC - ).count() - logger.info( - f"Deleted {len(ids_to_delete)} obsolete Works for provider {KUNTEC}" - ) - logger.info( - f"Created {num_created} Works of total {num_works} Works for provider {KUNTEC}." - ) - return num_created - - def handle(self, *args, **options): - super().__init__() - assert settings.KUNTEC_KEY, "KUNTEC_KEY not found in environment." - MaintenanceWork.objects.filter(maintenance_unit__provider=KUNTEC).delete() - history_size = KUNTEC_DEFAULT_WORKS_HISTORY_SIZE - if options["history_size"]: - history_size = int(options["history_size"][0]) - if history_size > KUNTEC_MAX_WORKS_HISTORY_SIZE: - error_msg = f"Max value for the history size is: {KUNTEC_MAX_WORKS_HISTORY_SIZE}" - raise ValueError(error_msg) - create_kuntec_maintenance_units() - works_created = self.create_kuntec_maintenance_works(history_size=history_size) - - # In some unknown(erroneous mapon server?) cases, there are no works with route and/or Unit with io_din - # Status 'On'(1) even if in reality there are. In that case we want to store the previeus state of the - # precalculated geometry history for Kuntec data. - if works_created > 0: - precalculate_geometry_history(KUNTEC) - else: - logger.warning( - f"No works created for {KUNTEC}, skipping geometry history population." - ) - super().display_duration(KUNTEC) diff --git a/street_maintenance/management/commands/import_yit_street_maintenance_history.py b/street_maintenance/management/commands/import_yit_street_maintenance_history.py deleted file mode 100644 index 34e4a9623..000000000 --- a/street_maintenance/management/commands/import_yit_street_maintenance_history.py +++ /dev/null @@ -1,140 +0,0 @@ -import logging - -from django.contrib.gis.geos import LineString - -from street_maintenance.models import DEFAULT_SRID, MaintenanceUnit, MaintenanceWork - -from .base_import_command import BaseImportCommand -from .constants import ( - EVENT_MAPPINGS, - YIT, - YIT_DEFAULT_WORKS_HISTORY_SIZE, - YIT_MAX_WORKS_HISTORY_SIZE, -) -from .utils import ( - create_dict_from_yit_events, - create_yit_maintenance_units, - get_linestring_in_boundary, - get_turku_boundary, - get_yit_access_token, - get_yit_contract, - get_yit_event_types, - get_yit_routes, - is_nested_coordinates, - precalculate_geometry_history, -) - -TURKU_BOUNDARY = get_turku_boundary() -logger = logging.getLogger("street_maintenance") - - -class Command(BaseImportCommand): - def add_arguments(self, parser): - parser.add_argument( - "--history-size", - type=int, - nargs="+", - default=False, - help="History size in days.", - ) - - def create_yit_maintenance_works(self, history_size=None): - access_token = get_yit_access_token() - create_yit_maintenance_units(access_token) - contract = get_yit_contract(access_token) - list_of_events = get_yit_event_types(access_token) - event_name_mappings = create_dict_from_yit_events(list_of_events) - routes = get_yit_routes(access_token, contract, history_size) - ids_to_delete = list( - MaintenanceUnit.objects.filter(provider=YIT).values_list("id", flat=True) - ) - num_created = 0 - for route in routes: - if len(route["geography"]["features"]) > 1: - logger.warning( - f"Route contains multiple features. {route['geography']['features']}" - ) - coordinates = route["geography"]["features"][0]["geometry"]["coordinates"] - if is_nested_coordinates(coordinates) and len(coordinates) > 1: - geometry = LineString(coordinates, srid=DEFAULT_SRID) - else: - # Remove other data, contains faulty linestrings. - continue - # Create linestring that is inside the boundary of Turku - # and discard parts of the geometry if they are outside the boundary. - geometry = get_linestring_in_boundary(geometry, TURKU_BOUNDARY) - if not geometry: - continue - events = [] - original_event_names = [] - operations = route["operations"] - for operation in operations: - event_name = event_name_mappings[operation].lower() - if event_name in EVENT_MAPPINGS: - for e in EVENT_MAPPINGS[event_name]: - # If mapping value is None, the event is not used. - if e: - events.append(e) - original_event_names.append(event_name_mappings[operation]) - else: - logger.warning( - f"Found unmapped event: {event_name_mappings[operation]}" - ) - - # If no events found discard the work - if len(events) == 0: - continue - if len(route["geography"]["features"]) > 1: - logger.warning( - f"Route contains multiple features. {route['geography']['features']}" - ) - unit_id = route["vehicleType"] - try: - unit = MaintenanceUnit.objects.get(unit_id=unit_id) - except MaintenanceUnit.DoesNotExist: - logger.warning(f"Maintenance unit: {unit_id}, not found.") - continue - - obj, created = MaintenanceWork.objects.get_or_create( - timestamp=route["startTime"], - maintenance_unit=unit, - events=events, - original_event_names=original_event_names, - geometry=geometry, - ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) - if created: - num_created += 1 - MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - num_works = MaintenanceWork.objects.filter( - maintenance_unit__provider=YIT - ).count() - logger.info(f"Deleted {len(ids_to_delete)} obsolete Works for provider {YIT}") - logger.info( - f"Created {num_created} Works of total {num_works} Works for provider {YIT}." - ) - return num_created - - def handle(self, *args, **options): - super().__init__() - MaintenanceWork.objects.filter(maintenance_unit__provider=YIT).delete() - history_size = YIT_DEFAULT_WORKS_HISTORY_SIZE - if options["history_size"]: - history_size = int(options["history_size"][0]) - if history_size > YIT_MAX_WORKS_HISTORY_SIZE: - error_msg = ( - f"Max value for the history size is: {YIT_MAX_WORKS_HISTORY_SIZE}" - ) - raise ValueError(error_msg) - - works_created = self.create_yit_maintenance_works(history_size=history_size) - # In some unknown(erroneous server?) cases no data for works is availale. In that case we want to store - # the previeus state of the precalculated geometry history data. - if works_created > 0: - precalculate_geometry_history(YIT) - else: - logger.warning( - f"No works created for {YIT}, skipping geometry history population." - ) - super().display_duration(YIT) From 1a07c8c1680956affb41e603bfb137ed833dff6d Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 23 Feb 2023 13:21:16 +0200 Subject: [PATCH 147/188] Add combined street maintenance history importer --- .../import_street_maintenance_history.py | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 street_maintenance/management/commands/import_street_maintenance_history.py diff --git a/street_maintenance/management/commands/import_street_maintenance_history.py b/street_maintenance/management/commands/import_street_maintenance_history.py new file mode 100644 index 000000000..cb2d2eb5f --- /dev/null +++ b/street_maintenance/management/commands/import_street_maintenance_history.py @@ -0,0 +1,132 @@ +import logging +from datetime import datetime + +from django.core.management import BaseCommand + +from street_maintenance.models import MaintenanceUnit, MaintenanceWork + +from .constants import ( + FETCH_SIZE, + HISTORY_SIZE, + HISTORY_SIZES, + PROVIDER_TYPES, + PROVIDERS, +) +from .utils import ( + create_kuntec_maintenance_units, + create_kuntec_maintenance_works, + create_maintenance_units, + create_maintenance_works, + create_yit_maintenance_units, + create_yit_maintenance_works, + get_yit_access_token, + precalculate_geometry_history, +) + +logger = logging.getLogger("street_maintenance") + + +class Command(BaseCommand): + def add_arguments(self, parser): + parser.add_argument( + "--fetch-size", + type=int, + nargs="+", + default=False, + help=("Max number of location history items to fetch per unit."), + ) + parser.add_argument( + "--history-size", + type=int, + nargs="+", + default=False, + help=("History size in days."), + ) + parser.add_argument( + "--providers", + type=str, + nargs="+", + default=False, + help=("History size in days."), + ) + + def handle(self, *args, **options): + history_size = None + fetch_size = None + + if options["history_size"]: + history_size = options["history_size"] + history_size = ( + history_size[0] if type(history_size) == list else history_size + ) + if options["fetch_size"]: + fetch_size = options["fetch_size"] + fetch_size = fetch_size[0] if type(fetch_size) == list else fetch_size + + providers = [p.upper() for p in options.get("providers", None)] + for provider in providers: + if provider not in PROVIDERS: + logger.warning( + f"Provider {provider} not defined, choices are {', '.join(PROVIDERS)}" + ) + continue + start_time = datetime.now() + history_size = ( + history_size if history_size else HISTORY_SIZES[provider][HISTORY_SIZE] + ) + fetch_size = ( + fetch_size + if fetch_size + else HISTORY_SIZES[provider].get(FETCH_SIZE, None) + ) + match provider.upper(): + case PROVIDER_TYPES.DESTIA | PROVIDER_TYPES.INFRAROAD: + num_created_units, num_del_units = create_maintenance_units( + provider + ) + num_created_works, num_del_works = create_maintenance_works( + provider, history_size, fetch_size + ) + case PROVIDER_TYPES.KUNTEC: + num_created_units, num_del_units = create_kuntec_maintenance_units() + num_created_works, num_del_works = create_kuntec_maintenance_works( + history_size + ) + + case PROVIDER_TYPES.YIT: + access_token = get_yit_access_token() + num_created_units, num_del_units = create_yit_maintenance_units( + access_token + ) + num_created_works, num_del_works = create_yit_maintenance_works( + access_token, history_size + ) + + tot_num_units = MaintenanceUnit.objects.filter(provider=provider).count() + tot_num_works = MaintenanceWork.objects.filter( + maintenance_unit__provider=provider + ).count() + logger.info( + f"Deleted {num_del_units} obsolete Units for provider {provider}" + ) + logger.info( + f"Created {num_created_units} units of total {tot_num_units} units for provider {provider}." + ) + logger.info( + f"Deleted {num_del_works} obsolete Works for provider {provider}" + ) + logger.info( + f"Created {num_created_works} Works of total {tot_num_works} Works for provider {provider}." + ) + + if num_created_works > 0: + precalculate_geometry_history(provider) + else: + logger.warning( + f"No works created for {provider}, skipping geometry history population." + ) + end_time = datetime.now() + duration = end_time - start_time + logger.info( + f"Imported {provider} street maintenance history in: {duration}" + ) From b349a89f8195c37fa4b58c352401fd32cfe0e0ce Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 23 Feb 2023 13:22:23 +0200 Subject: [PATCH 148/188] Add task to import street maintenance history --- street_maintenance/tasks.py | 69 +++++++++++-------------------------- 1 file changed, 20 insertions(+), 49 deletions(-) diff --git a/street_maintenance/tasks.py b/street_maintenance/tasks.py index db905964b..021e1f2af 100644 --- a/street_maintenance/tasks.py +++ b/street_maintenance/tasks.py @@ -1,57 +1,28 @@ -from celery import shared_task from django.core import management - -@shared_task -def delete_street_maintenance_history( - args=None, name="delete_street_maintenance_history" -): - management.call_command("delete_street_maintenance_history", args) - - -@shared_task -def import_infraroad_street_maintenance_history( - args=None, name="import_infraroad_street_maintenance_history" -): - if args: - management.call_command( - "import_infraroad_street_maintenance_history", "--history-size", args - ) - else: - management.call_command("import_infraroad_street_maintenance_history") +from smbackend.utils import shared_task_email -@shared_task -def import_yit_street_maintenance_history( - args=None, name="import_yit_street_maintenance_history" -): - if args: - management.call_command( - "import_yit_street_maintenance_history", "--history-size", args - ) - else: - management.call_command("import_yit_street_maintenance_history") - - -@shared_task -def import_kuntec_street_maintenance_history( - args=None, name="import_kuntec_street_maintenance_history" -): - if args: - management.call_command( - "import_kuntec_street_maintenance_history", "--history-size", args - ) - else: - management.call_command("import_kuntec_street_maintenance_history") +@shared_task_email +def delete_street_maintenance_history(args, name="delete_street_maintenance_history"): + management.call_command("delete_street_maintenance_history", args) -@shared_task -def import_destia_street_maintenance_history( - args=None, name="import_destia_street_maintenance_history" +@shared_task_email +def import_street_maintenance_history( + name="import_street_maintenance_history", *args, **kwargs ): - if args: - management.call_command( - "import_destia_maintenance_history", "--history-size", args + if "providers" not in kwargs: + raise Exception( + "No 'providers' item in kwargs. e.g., {'providers':['destia', 'infraroad']}" ) - else: - management.call_command("import_destia_street_maintenance_history") + if "fetch-size" not in kwargs: + kwargs["fetch-size"] = None + if "history-size" not in kwargs: + kwargs["history-size"] = None + management.call_command( + "import_street_maintenance_history", + providers=kwargs["providers"], + fetch_size=kwargs["fetch-size"], + history_size=kwargs["history-size"], + ) From 6fae4a7ae9acc24f41f80d25ab1e27f1b07a761d Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 23 Feb 2023 13:22:58 +0200 Subject: [PATCH 149/188] Add functions to create YIT and Kuntec works, uniform return values of create works and units functions --- .../management/commands/utils.py | 206 ++++++++++++++---- 1 file changed, 167 insertions(+), 39 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index fe66d6d56..2faa2ee2a 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -4,6 +4,7 @@ from datetime import datetime, timedelta import numpy as np +import polyline import requests from django import db from django.conf import settings @@ -22,6 +23,7 @@ EVENT_MAPPINGS, EVENTS, KUNTEC, + KUNTEC_KEY, PROVIDERS, ROUTES, TIMESTAMP_FORMATS, @@ -39,6 +41,18 @@ VALID_LINESTRING_MAX_POINT_DISTANCE = 0.01 +def get_turku_boundary(): + division_turku = AdministrativeDivision.objects.get(name="Turku") + turku_boundary = AdministrativeDivisionGeometry.objects.get( + division=division_turku + ).boundary + turku_boundary.transform(DEFAULT_SRID) + return turku_boundary + + +TURKU_BOUNDARY = get_turku_boundary() + + def check_linestring_validity( linestring, threshold=VALID_LINESTRING_MAX_POINT_DISTANCE ): @@ -200,15 +214,6 @@ def precalculate_geometry_history(provider): logger.info(f"Created {len(objects)} HistoryGeometry rows for provider: {provider}") -def get_turku_boundary(): - division_turku = AdministrativeDivision.objects.get(name="Turku") - turku_boundary = AdministrativeDivisionGeometry.objects.get( - division=division_turku - ).boundary - turku_boundary.transform(DEFAULT_SRID) - return turku_boundary - - def get_linestring_in_boundary(linestring, boundary): """ Returns a linestring from the input linestring where all the coordinates @@ -226,6 +231,146 @@ def get_linestring_in_boundary(linestring, boundary): return False +@db.transaction.atomic +def create_yit_maintenance_works(access_token, history_size): + contract = get_yit_contract(access_token) + list_of_events = get_yit_event_types(access_token) + event_name_mappings = create_dict_from_yit_events(list_of_events) + routes = get_yit_routes(access_token, contract, history_size) + ids_to_delete = list( + MaintenanceWork.objects.filter(maintenance_unit__provider=YIT).values_list( + "id", flat=True + ) + ) + num_created = 0 + for route in routes: + if len(route["geography"]["features"]) > 1: + logger.warning( + f"Route contains multiple features. {route['geography']['features']}" + ) + coordinates = route["geography"]["features"][0]["geometry"]["coordinates"] + if is_nested_coordinates(coordinates) and len(coordinates) > 1: + geometry = LineString(coordinates, srid=DEFAULT_SRID) + else: + # Remove other data, contains faulty linestrings. + continue + # Create linestring that is inside the boundary of Turku + # and discard parts of the geometry if they are outside the boundary. + geometry = get_linestring_in_boundary(geometry, TURKU_BOUNDARY) + if not geometry: + continue + events = [] + original_event_names = [] + operations = route["operations"] + for operation in operations: + event_name = event_name_mappings[operation].lower() + if event_name in EVENT_MAPPINGS: + for e in EVENT_MAPPINGS[event_name]: + # If mapping value is None, the event is not used. + if e: + events.append(e) + original_event_names.append(event_name_mappings[operation]) + else: + logger.warning( + f"Found unmapped event: {event_name_mappings[operation]}" + ) + + # If no events found discard the work + if len(events) == 0: + continue + if len(route["geography"]["features"]) > 1: + logger.warning( + f"Route contains multiple features. {route['geography']['features']}" + ) + unit_id = route["vehicleType"] + try: + unit = MaintenanceUnit.objects.get(unit_id=unit_id) + except MaintenanceUnit.DoesNotExist: + logger.warning(f"Maintenance unit: {unit_id}, not found.") + continue + + obj, created = MaintenanceWork.objects.get_or_create( + timestamp=route["startTime"], + maintenance_unit=unit, + events=events, + original_event_names=original_event_names, + geometry=geometry, + ) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() + return num_created, len(ids_to_delete) + + +@db.transaction.atomic +def create_kuntec_maintenance_works(history_size): + num_created = 0 + now = datetime.now() + start = (now - timedelta(days=history_size)).strftime(TIMESTAMP_FORMATS[KUNTEC]) + end = now.strftime(TIMESTAMP_FORMATS[KUNTEC]) + ids_to_delete = list( + MaintenanceWork.objects.filter(maintenance_unit__provider=KUNTEC).values_list( + "id", flat=True + ) + ) + for unit in MaintenanceUnit.objects.filter(provider=KUNTEC): + url = URLS[KUNTEC][WORKS].format( + key=KUNTEC_KEY, start=start, end=end, unit_id=unit.unit_id + ) + response = requests.get(url) + if response.status_code != 200: + continue + if "data" in response.json(): + for units in response.json()["data"]["units"]: + for route in units["routes"]: + events = [] + original_event_names = [] + # Routes of type 'stop' are discarded. + if route["type"] == "route": + # Check for mapped events to include as works. + for name in unit.names: + event_name = name.lower() + if event_name in EVENT_MAPPINGS: + for e in EVENT_MAPPINGS[event_name]: + # If mapping value is None, the event is not used. + if e: + events.append(e) + original_event_names.append(name) + else: + logger.warning(f"Found unmapped event: {event_name}") + # If route has mapped event(s) and contains a polyline add work. + if len(events) > 0 and "polyline" in route: + coords = polyline.decode(route["polyline"], geojson=True) + if len(coords) > 1: + geometry = LineString(coords, srid=DEFAULT_SRID) + else: + continue + # Create linestring that is inside the boundary of Turku + # and discard parts of the geometry if they are outside the boundary. + geometry = get_linestring_in_boundary(geometry, TURKU_BOUNDARY) + if not geometry: + continue + timestamp = route["start"]["time"] + + obj, created = MaintenanceWork.objects.get_or_create( + timestamp=timestamp, + maintenance_unit=unit, + events=events, + original_event_names=original_event_names, + geometry=geometry, + ) + if obj.id in ids_to_delete: + ids_to_delete.remove(obj.id) + if created: + num_created += 1 + + MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() + return num_created, len(ids_to_delete) + + +@db.transaction.atomic def create_maintenance_works(provider, history_size, fetch_size): turku_boundary = get_turku_boundary() num_created = 0 @@ -234,6 +379,11 @@ def create_maintenance_works(provider, history_size, fetch_size): import_from_date_time = import_from_date_time.replace( tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki") ) + ids_to_delete = list( + MaintenanceWork.objects.filter(maintenance_unit__provider=provider).values_list( + "id", flat=True + ) + ) for unit in MaintenanceUnit.objects.filter(provider=provider): response = requests.get( URLS[provider][WORKS].format(id=unit.unit_id, history_size=fetch_size) @@ -243,13 +393,8 @@ def create_maintenance_works(provider, history_size, fetch_size): else: logger.warning(f"Location history not found for unit: {unit.unit_id}") continue - ids_to_delete = list( - MaintenanceUnit.objects.filter(provider=provider).values_list( - "id", flat=True - ) - ) - for work in json_data: + for work in json_data: timestamp = datetime.strptime( work["timestamp"], TIMESTAMP_FORMATS[provider] ).replace(tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki")) @@ -291,16 +436,10 @@ def create_maintenance_works(provider, history_size, fetch_size): if created: num_created += 1 MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - num_works = MaintenanceWork.objects.filter( - maintenance_unit__provider=provider - ).count() - logger.info(f"Deleted {len(ids_to_delete)} obsolete Works for provider {provider}") - logger.info( - f"Created {num_created} Works of total {num_works} Works for provider {provider}." - ) - return num_created + return num_created, len(ids_to_delete) +@db.transaction.atomic def create_maintenance_units(provider): num_created = 0 assert provider in PROVIDERS @@ -325,12 +464,7 @@ def create_maintenance_units(provider): num_created += 1 MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() - num_units = MaintenanceUnit.objects.filter(provider=provider).count() - logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {provider}") - logger.info( - f"Created {num_created} units of total {num_units} units for provider {provider}." - ) - return num_units + return num_created, len(ids_to_delete) def get_yit_contract(access_token): @@ -364,6 +498,7 @@ def create_dict_from_yit_events(list_of_events): return events +@db.transaction.atomic def create_kuntec_maintenance_units(): units_url = URLS[KUNTEC][UNITS] response = requests.get(units_url) @@ -401,16 +536,13 @@ def create_kuntec_maintenance_units(): else: no_io_din += 1 MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() - logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {KUNTEC}") logger.info( f"Discarding {no_io_din} Kuntec units that do not have a io_din with Status 'On'(1)." ) - num_units = MaintenanceUnit.objects.filter(provider=KUNTEC).count() - logger.info( - f"Created {num_created} units of total {num_units} units for provider {KUNTEC}." - ) + return num_created, len(ids_to_delete) +@db.transaction.atomic def create_yit_maintenance_units(access_token): response = requests.get( URLS[YIT][VEHICLES], headers={"Authorization": f"Bearer {access_token}"} @@ -434,11 +566,7 @@ def create_yit_maintenance_units(access_token): if created: num_created += 1 MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() - logger.info(f"Deleted {len(ids_to_delete)} obsolete Units for provider {YIT}") - num_units = MaintenanceUnit.objects.filter(provider=YIT).count() - logger.info( - f"Created {num_created} units of total {num_units} units for provider {YIT}." - ) + return num_created, len(ids_to_delete) def get_yit_routes(access_token, contract, history_size): From b329615bedad9e7cd56e0d2b3944ebcc4ced2a93 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 23 Feb 2023 13:24:52 +0200 Subject: [PATCH 150/188] Add HISTORY_SIZES and PROVIDER_TYPES --- .../management/commands/constants.py | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/street_maintenance/management/commands/constants.py b/street_maintenance/management/commands/constants.py index 10377d001..bc28f9638 100644 --- a/street_maintenance/management/commands/constants.py +++ b/street_maintenance/management/commands/constants.py @@ -1,3 +1,5 @@ +import types + from django.conf import settings KUNTEC_KEY = settings.KUNTEC_KEY @@ -12,6 +14,11 @@ (DESTIA, "Destia"), ) PROVIDERS = [INFRAROAD, YIT, KUNTEC, DESTIA] +PROVIDER_TYPES = types.SimpleNamespace() +PROVIDER_TYPES.YIT = YIT +PROVIDER_TYPES.KUNTEC = KUNTEC +PROVIDER_TYPES.DESTIA = DESTIA +PROVIDER_TYPES.INFRAROAD = INFRAROAD UNITS = "UNITS" WORKS = "WORKS" @@ -141,7 +148,7 @@ } # GeometryHistory API list start_date_time parameter format. START_DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" -# The number of works(point data with timestamp and event) to be fetched for every unit. +# The number of works(point data with a timestamp and events) to be fetched for every unit. INFRAROAD_DEFAULT_WORKS_FETCH_SIZE = 10000 DESTIA_DEFAULT_WORKS_FETCH_SIZE = 10000 # In days, Note if value is increased the fetch size should also be increased. @@ -154,3 +161,17 @@ KUNTEC_DEFAULT_WORKS_HISTORY_SIZE = 4 KUNTEC_MAX_WORKS_HISTORY_SIZE = 31 +HISTORY_SIZE = "history_size" +FETCH_SIZE = "fetch_size" +HISTORY_SIZES = { + INFRAROAD: { + HISTORY_SIZE: INFRAROAD_DEFAULT_WORKS_HISTORY_SIZE, + FETCH_SIZE: INFRAROAD_DEFAULT_WORKS_FETCH_SIZE, + }, + DESTIA: { + HISTORY_SIZE: DESTIA_DEFAULT_WORKS_HISTORY_SIZE, + FETCH_SIZE: DESTIA_DEFAULT_WORKS_FETCH_SIZE, + }, + KUNTEC: {HISTORY_SIZE: KUNTEC_DEFAULT_WORKS_HISTORY_SIZE}, + YIT: {HISTORY_SIZE: YIT_DEFAULT_WORKS_HISTORY_SIZE}, +} From 45445962ac2119260420c4170d4db1a9774074b7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 24 Feb 2023 13:24:50 +0200 Subject: [PATCH 151/188] Add and use function to get json data --- .../management/commands/utils.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index 2faa2ee2a..c8a9a8654 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -53,6 +53,14 @@ def get_turku_boundary(): TURKU_BOUNDARY = get_turku_boundary() +def get_json_data(url): + response = requests.get(url) + assert ( + response.status_code == 200 + ), "Fetching Maintenance Unit {} status code: {}".format(url, response.status_code) + return response.json() + + def check_linestring_validity( linestring, threshold=VALID_LINESTRING_MAX_POINT_DISTANCE ): @@ -385,15 +393,15 @@ def create_maintenance_works(provider, history_size, fetch_size): ) ) for unit in MaintenanceUnit.objects.filter(provider=provider): - response = requests.get( + json_data = get_json_data( URLS[provider][WORKS].format(id=unit.unit_id, history_size=fetch_size) ) - if "location_history" in response.json(): - json_data = response.json()["location_history"] + if "location_history" in json_data: + json_data = json_data["location_history"] else: logger.warning(f"Location history not found for unit: {unit.unit_id}") continue - + breakpoint() for work in json_data: timestamp = datetime.strptime( work["timestamp"], TIMESTAMP_FORMATS[provider] @@ -442,17 +450,10 @@ def create_maintenance_works(provider, history_size, fetch_size): @db.transaction.atomic def create_maintenance_units(provider): num_created = 0 - assert provider in PROVIDERS - response = requests.get(URLS[provider][UNITS]) - assert ( - response.status_code == 200 - ), "Fetching Maintenance Unit {} status code: {}".format( - URLS[provider][UNITS], response.status_code - ) ids_to_delete = list( MaintenanceUnit.objects.filter(provider=provider).values_list("id", flat=True) ) - for unit in response.json(): + for unit in get_json_data(URLS[provider][UNITS]): # The names of the unit is derived from the events. names = [n for n in unit["last_location"]["events"]] obj, created = MaintenanceUnit.objects.get_or_create( From a83518216d6860198221057dd8e88edfe5a287ce Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 24 Feb 2023 13:25:41 +0200 Subject: [PATCH 152/188] Add administrative division fixtures --- street_maintenance/tests/conftest.py | 38 +++++++++++++++++++++++++--- 1 file changed, 35 insertions(+), 3 deletions(-) diff --git a/street_maintenance/tests/conftest.py b/street_maintenance/tests/conftest.py index 14e0667eb..0649403c2 100644 --- a/street_maintenance/tests/conftest.py +++ b/street_maintenance/tests/conftest.py @@ -2,9 +2,15 @@ import pytest import pytz -from django.contrib.gis.geos import LineString +from django.contrib.gis.geos import GEOSGeometry, LineString +from munigeo.models import ( + AdministrativeDivision, + AdministrativeDivisionGeometry, + AdministrativeDivisionType, +) from rest_framework.test import APIClient +from mobility_data.tests.conftest import TURKU_WKT from street_maintenance.management.commands.constants import ( AURAUS, INFRAROAD, @@ -69,5 +75,31 @@ def geometry_historys(): provider=KUNTEC, events=[AURAUS, LIUKKAUDENTORJUNTA], ) - geometry_historys.append(obj) - return geometry_historys + geometry_historys.append(obj) @ pytest.mark.django_db + + +@pytest.fixture +def administrative_division_type(): + adm_div_type = AdministrativeDivisionType.objects.create( + id=1, type="muni", name="Municipality" + ) + return adm_div_type + + +@pytest.mark.django_db +@pytest.fixture +def administrative_division(administrative_division_type): + adm_div = AdministrativeDivision.objects.get_or_create( + id=1, name="Turku", origin_id=853, type_id=1 + ) + return adm_div + + +@pytest.mark.django_db +@pytest.fixture +def administrative_division_geometry(administrative_division): + turku_multipoly = GEOSGeometry(TURKU_WKT, srid=3067) + adm_div_geom = AdministrativeDivisionGeometry.objects.create( + id=1, division_id=1, boundary=turku_multipoly + ) + return adm_div_geom From 6162041d5f0c644e3baa347c7e56659eedb76683 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 24 Feb 2023 15:00:10 +0200 Subject: [PATCH 153/188] Add DATE_FORMATS constant --- street_maintenance/management/commands/constants.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/street_maintenance/management/commands/constants.py b/street_maintenance/management/commands/constants.py index bc28f9638..96254bce6 100644 --- a/street_maintenance/management/commands/constants.py +++ b/street_maintenance/management/commands/constants.py @@ -146,6 +146,12 @@ KUNTEC: "%Y-%m-%dT%H:%M:%SZ", YIT: "%Y-%m-%d %H:%M:%S%z", } +DATE_FORMATS = { + INFRAROAD: "%Y-%m-%d", + DESTIA: "%Y-%m-%d", + KUNTEC: "%Y-%m-%d", + YIT: "%Y-%m-%d", +} # GeometryHistory API list start_date_time parameter format. START_DATE_TIME_FORMAT = "%Y-%m-%d %H:%M:%S" # The number of works(point data with a timestamp and events) to be fetched for every unit. From 77d1f5550519bb34d77309b9f6676878f3a87e2a Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 24 Feb 2023 15:00:56 +0200 Subject: [PATCH 154/188] Change name of variable ids_to_delete to objs_to_delete --- .../management/commands/utils.py | 63 +++++++++---------- 1 file changed, 31 insertions(+), 32 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index c8a9a8654..eded88131 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -24,7 +24,6 @@ EVENTS, KUNTEC, KUNTEC_KEY, - PROVIDERS, ROUTES, TIMESTAMP_FORMATS, TOKEN, @@ -245,7 +244,7 @@ def create_yit_maintenance_works(access_token, history_size): list_of_events = get_yit_event_types(access_token) event_name_mappings = create_dict_from_yit_events(list_of_events) routes = get_yit_routes(access_token, contract, history_size) - ids_to_delete = list( + objs_to_delete = list( MaintenanceWork.objects.filter(maintenance_unit__provider=YIT).values_list( "id", flat=True ) @@ -304,12 +303,12 @@ def create_yit_maintenance_works(access_token, history_size): original_event_names=original_event_names, geometry=geometry, ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 - MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - return num_created, len(ids_to_delete) + MaintenanceWork.objects.filter(id__in=objs_to_delete).delete() + return num_created, len(objs_to_delete) @db.transaction.atomic @@ -318,7 +317,7 @@ def create_kuntec_maintenance_works(history_size): now = datetime.now() start = (now - timedelta(days=history_size)).strftime(TIMESTAMP_FORMATS[KUNTEC]) end = now.strftime(TIMESTAMP_FORMATS[KUNTEC]) - ids_to_delete = list( + objs_to_delete = list( MaintenanceWork.objects.filter(maintenance_unit__provider=KUNTEC).values_list( "id", flat=True ) @@ -369,13 +368,13 @@ def create_kuntec_maintenance_works(history_size): original_event_names=original_event_names, geometry=geometry, ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 - MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - return num_created, len(ids_to_delete) + MaintenanceWork.objects.filter(id__in=objs_to_delete).delete() + return num_created, len(objs_to_delete) @db.transaction.atomic @@ -387,7 +386,7 @@ def create_maintenance_works(provider, history_size, fetch_size): import_from_date_time = import_from_date_time.replace( tzinfo=zoneinfo.ZoneInfo("Europe/Helsinki") ) - ids_to_delete = list( + objs_to_delete = list( MaintenanceWork.objects.filter(maintenance_unit__provider=provider).values_list( "id", flat=True ) @@ -401,7 +400,6 @@ def create_maintenance_works(provider, history_size, fetch_size): else: logger.warning(f"Location history not found for unit: {unit.unit_id}") continue - breakpoint() for work in json_data: timestamp = datetime.strptime( work["timestamp"], TIMESTAMP_FORMATS[provider] @@ -439,18 +437,19 @@ def create_maintenance_works(provider, history_size, fetch_size): events=events, original_event_names=original_event_names, ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 - MaintenanceWork.objects.filter(id__in=ids_to_delete).delete() - return num_created, len(ids_to_delete) + + MaintenanceWork.objects.filter(id__in=objs_to_delete).delete() + return num_created, len(objs_to_delete) @db.transaction.atomic def create_maintenance_units(provider): num_created = 0 - ids_to_delete = list( + objs_to_delete = list( MaintenanceUnit.objects.filter(provider=provider).values_list("id", flat=True) ) for unit in get_json_data(URLS[provider][UNITS]): @@ -459,13 +458,13 @@ def create_maintenance_units(provider): obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit["id"], names=names, provider=provider ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 - MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() - return num_created, len(ids_to_delete) + MaintenanceUnit.objects.filter(id__in=objs_to_delete).delete() + return num_created, len(objs_to_delete) def get_yit_contract(access_token): @@ -510,7 +509,7 @@ def create_kuntec_maintenance_units(): ) no_io_din = 0 num_created = 0 - ids_to_delete = list( + objs_to_delete = list( MaintenanceUnit.objects.filter(provider=KUNTEC).values_list("id", flat=True) ) @@ -529,18 +528,18 @@ def create_kuntec_maintenance_units(): obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit_id, names=names, provider=KUNTEC ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 else: no_io_din += 1 - MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() + MaintenanceUnit.objects.filter(id__in=objs_to_delete).delete() logger.info( f"Discarding {no_io_din} Kuntec units that do not have a io_din with Status 'On'(1)." ) - return num_created, len(ids_to_delete) + return num_created, len(objs_to_delete) @db.transaction.atomic @@ -554,7 +553,7 @@ def create_yit_maintenance_units(access_token): URLS[YIT][VEHICLES], response.status_code ) num_created = 0 - ids_to_delete = list( + objs_to_delete = list( MaintenanceUnit.objects.filter(provider=YIT).values_list("id", flat=True) ) for unit in response.json(): @@ -562,12 +561,12 @@ def create_yit_maintenance_units(access_token): obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit["id"], names=names, provider=YIT ) - if obj.id in ids_to_delete: - ids_to_delete.remove(obj.id) + if obj.id in objs_to_delete: + objs_to_delete.remove(obj.id) if created: num_created += 1 - MaintenanceUnit.objects.filter(id__in=ids_to_delete).delete() - return num_created, len(ids_to_delete) + MaintenanceUnit.objects.filter(id__in=objs_to_delete).delete() + return num_created, len(objs_to_delete) def get_yit_routes(access_token, contract, history_size): From ab3455643bd8cc6a325a1fff7a377da0a4ce7b72 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 24 Feb 2023 15:01:39 +0200 Subject: [PATCH 155/188] Add infraroad tests --- street_maintenance/tests/test_importers.py | 96 ++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 street_maintenance/tests/test_importers.py diff --git a/street_maintenance/tests/test_importers.py b/street_maintenance/tests/test_importers.py new file mode 100644 index 000000000..904384b9d --- /dev/null +++ b/street_maintenance/tests/test_importers.py @@ -0,0 +1,96 @@ +from datetime import datetime +from unittest.mock import patch + +import pytest + +from street_maintenance.management.commands.constants import DATE_FORMATS, INFRAROAD +from street_maintenance.models import MaintenanceUnit, MaintenanceWork + + +def get_infraroad_works_json_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) + location_history = [ + { + "timestamp": f"{current_date} 08:29:49", + "coords": "(22.24957474 60.49515401)", + "events": ["au"], + }, + { + "timestamp": f"{current_date} 08:29:28", + "coords": "(22.24946401 60.49515848)", + "events": ["au"], + }, + { + "timestamp": f"{current_date} 08:28:32", + "coords": "(22.24944127 60.49519463)", + "events": ["hiekoitus"], + }, + ] + assert num_elements <= len(location_history) + data = {"location_history": location_history[:num_elements]} + return data + + +def get_infraroad_units_json_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) + data = [ + { + "id": 2817625, + "last_location": { + "timestamp": f"{current_date} 06:31:34", + "coords": "(22.249642023816705 60.49569119699299)", + "events": ["au"], + }, + }, + { + "id": 12891825, + "last_location": { + "timestamp": f"{current_date} 08:29:49", + "coords": "(22.24957474 60.49515401)", + "events": ["Kenttien hoito"], + }, + }, + ] + assert num_elements <= len(data) + return data[:num_elements] + + +@pytest.mark.django_db +@patch("street_maintenance.management.commands.utils.get_json_data") +def test_infraroad( + get_json_data_mock, administrative_division, administrative_division_geometry +): + from street_maintenance.management.commands.utils import ( + create_maintenance_units, + create_maintenance_works, + ) + + # Test unit creation + get_json_data_mock.return_value = get_infraroad_units_json_data(2) + num_created_units, num_del_units = create_maintenance_units(INFRAROAD) + assert MaintenanceUnit.objects.count() == 2 + assert num_created_units == 2 + assert num_del_units == 0 + unit = MaintenanceUnit.objects.first() + unit.unit_id = "2817625" + unit.names = ["au"] + get_json_data_mock.return_value = get_infraroad_units_json_data(1) + num_created_units, num_del_units = create_maintenance_units(INFRAROAD) + assert unit.id == MaintenanceUnit.objects.first().id + assert num_created_units == 0 + assert num_del_units == 1 + assert MaintenanceUnit.objects.count() == 1 + get_json_data_mock.return_value = get_infraroad_works_json_data(3) + num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) + assert num_created_works == 3 + assert num_del_works == 0 + assert MaintenanceWork.objects.count() == 3 + work = MaintenanceWork.objects.first() + work.events = ["auraus"] + work.original_event_names = ["au"] + get_json_data_mock.return_value = get_infraroad_works_json_data(1) + num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) + assert num_created_works == 0 + assert num_del_works == 2 + assert work.id == MaintenanceWork.objects.first().id + assert MaintenanceWork.objects.count() == 1 From d6429550353cf12ac04a5fedb9c2b7cd298c2115 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 27 Feb 2023 10:36:45 +0200 Subject: [PATCH 156/188] Kuntec functions use get_json_data to get json data --- .../management/commands/utils.py | 21 ++++++------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index eded88131..8d969dae3 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -326,12 +326,10 @@ def create_kuntec_maintenance_works(history_size): url = URLS[KUNTEC][WORKS].format( key=KUNTEC_KEY, start=start, end=end, unit_id=unit.unit_id ) - response = requests.get(url) - if response.status_code != 200: - continue - if "data" in response.json(): - for units in response.json()["data"]["units"]: - for route in units["routes"]: + json_data = get_json_data(url) + if "data" in json_data: + for unit_data in json_data["data"]["units"]: + for route in unit_data["routes"]: events = [] original_event_names = [] # Routes of type 'stop' are discarded. @@ -500,20 +498,13 @@ def create_dict_from_yit_events(list_of_events): @db.transaction.atomic def create_kuntec_maintenance_units(): - units_url = URLS[KUNTEC][UNITS] - response = requests.get(units_url) - assert ( - response.status_code == 200 - ), "Fetching Maintenance Unit {} status code: {}".format( - units_url, response.status_code - ) + json_data = get_json_data(URLS[KUNTEC][UNITS]) no_io_din = 0 num_created = 0 objs_to_delete = list( MaintenanceUnit.objects.filter(provider=KUNTEC).values_list("id", flat=True) ) - - for unit in response.json()["data"]["units"]: + for unit in json_data["data"]["units"]: names = [] if "io_din" in unit: on_states = 0 From 384b98c4b6625951f87545a3eba5b9474eaf9edf Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 27 Feb 2023 10:37:54 +0200 Subject: [PATCH 157/188] Add functions that returns fixture JSON data --- street_maintenance/tests/utils.py | 254 ++++++++++++++++++++++++++++++ 1 file changed, 254 insertions(+) create mode 100644 street_maintenance/tests/utils.py diff --git a/street_maintenance/tests/utils.py b/street_maintenance/tests/utils.py new file mode 100644 index 000000000..e48e54c15 --- /dev/null +++ b/street_maintenance/tests/utils.py @@ -0,0 +1,254 @@ +from datetime import datetime + +from street_maintenance.management.commands.constants import ( + DATE_FORMATS, + INFRAROAD, +) + + +def get_infraroad_works_json_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) + location_history = [ + { + "timestamp": f"{current_date} 08:29:49", + "coords": "(22.24957474 60.49515401)", + "events": ["au"], + }, + { + "timestamp": f"{current_date} 08:29:28", + "coords": "(22.24946401 60.49515848)", + "events": ["au"], + }, + { + "timestamp": f"{current_date} 08:28:32", + "coords": "(22.24944127 60.49519463)", + "events": ["hiekoitus"], + }, + ] + assert num_elements <= len(location_history) + data = {"location_history": location_history[:num_elements]} + return data + + +def get_infraroad_units_json_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) + data = [ + { + "id": 2817625, + "last_location": { + "timestamp": f"{current_date} 06:31:34", + "coords": "(22.249642023816705 60.49569119699299)", + "events": ["au"], + }, + }, + { + "id": 12891825, + "last_location": { + "timestamp": f"{current_date} 08:29:49", + "coords": "(22.24957474 60.49515401)", + "events": ["Kenttien hoito"], + }, + }, + ] + assert num_elements <= len(data) + return data[:num_elements] + + +def get_kuntec_works_json_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) + routes = [ + { + "route_id": 3980827390, + "type": "route", + "start": { + "time": f"{current_date}T14:54:31Z", + "address": "M\u00e4likk\u00e4l\u00e4, 21280 Turku, Suomi", + "lat": 60.47185, + "lng": 22.21618, + }, + "avg_speed": 18, + "max_speed": 43, + "end": { + "time": f"{current_date}T15:05:56Z", + "address": "Ihalantie 7, 21200 Raisio, Suomi", + "lat": 60.47945, + "lng": 22.18221, + }, + "distance": 3565, + "polyline": "a|apJcbrfC@HEpQYdEi@rDeAnE{Sji@eOjp@qEpb@uUlkA?T@TFRHLNDb@@b@Cd@Sb@i@XeArCeJv@{@z@c@h@E~@JdC" + + "n@FCBE@C@G@G@K?KEsA@cBAI?K?}BBWF[DOBMJQBCB?D?JHFHDN@FBNBLBD@BDBES?EAEAEI[CM@EXt@Uw@Tz@BHA?EQEMO_@@HIII" + + "IAE?QAc@EXCECBEDAHWdBENAF?D?D?v@DhE@JCQ?M@M?_@@oFBKF]", + }, + { + "route_id": 3984019243, + "type": "route", + "start": { + "time": f"{current_date}T11:16:00Z", + "address": "Tuontikatu 180, 20200 Turku, Suomi", + "lat": 60.44447, + "lng": 22.21462, + }, + "avg_speed": 18, + "max_speed": 43, + "end": { + "time": f"{current_date}T11:21:55Z", + "address": "Tuontikatu, 20200 Turku, Suomi", + "lat": 60.44754, + "lng": 22.21391, + }, + "distance": 1799, + "polyline": "}p|oJkxqfCt@|AtAtELT@JFd@d@nBX`AfAlFLl@Rv@xHv[T^p@~@DJ@B?DDPD@KUACCIQUg@i@]q@m@_C{Loi@AUIKCEU" + + "_@Oc@{@yCw@aB_BaB_A_@_CI{F[IFGLI^A`@?d@IfCGxA", + }, + ] + + assert num_elements <= len(routes) + data = {"data": {"units": [{"routes": routes[:num_elements]}]}} + return data + + +def get_kuntec_units_json_data(num_elements): + null = None + units = [ + { + "unit_id": 150635, + "box_id": 27953713, + "company_id": 5495, + "country_code": "FI", + "label": "1100781186", + "number": "HAKA 2", + "shortcut": "", + "vehicle_title": null, + "car_reg_certificate": "", + "vin": null, + "type": "car", + "icon": "tractor", + "lat": 60.46423, + "lng": 22.43703, + "direction": 161, + "speed": null, + "mileage": 11779869, + "last_update": "2023-02-25T13:20:56Z", + "ignition_total_time": 7683589, + "state": { + "name": "nodata", + "start": "2023-02-25T12:40:53Z", + "duration": 148756, + "debug_info": { + "boxId": 27953713, + "carId": 150635, + "msg": "POWEROFF", + "lastUpdate": 1677331256, + "lastValues": null, + }, + }, + "movement_state": { + "name": "nodata", + "start": "2023-02-25T12:40:53Z", + "duration": 151159, + }, + "fuel_type": "", + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "created_at": "2019-11-05T10:10:38Z", + "io_din": [ + {"no": 1, "label": "Auraus", "state": 1}, + {"no": 2, "label": "Hiekoitus", "state": 0}, + {"no": 3, "label": "Muu ty\u00f6", "state": 0}, + ], + }, + { + "unit_id": 150662, + "box_id": 27953746, + "company_id": 5495, + "country_code": "FI", + "label": "1101049692", + "number": "TAKKU 1", + "shortcut": "", + "vehicle_title": null, + "car_reg_certificate": "", + "vin": null, + "type": "car", + "icon": "tractor", + "lat": 60.55185, + "lng": 22.20567, + "direction": 213, + "speed": null, + "mileage": 10537795, + "last_update": "2023-02-26T10:02:03Z", + "ignition_total_time": 0, + "state": { + "name": "nodata", + "start": "2023-02-26T09:33:37Z", + "duration": 74289, + "debug_info": { + "boxId": 27953746, + "carId": 150662, + "msg": "OTHER", + "lastUpdate": 1677405723, + "lastValues": {"VOLTAGE": "14289"}, + }, + }, + "movement_state": { + "name": "nodata", + "start": "2023-02-26T09:33:37Z", + "duration": 75995, + }, + "fuel_type": "", + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "created_at": "2019-11-05T10:39:46Z", + "io_din": [ + {"no": 1, "label": "Auraus", "state": 1}, + {"no": 2, "label": "Hiekoitus", "state": 0}, + {"no": 3, "label": "Muu ty\u00f6", "state": 0}, + ], + }, + { + "unit_id": 150662, + "box_id": 27953746, + "company_id": 5495, + "country_code": "FI", + "label": "1101049692", + "number": "TAKKU 1", + "shortcut": "", + "vehicle_title": null, + "car_reg_certificate": "", + "vin": null, + "type": "car", + "icon": "tractor", + "lat": 60.55185, + "lng": 22.20567, + "direction": 213, + "speed": null, + "mileage": 10537795, + "last_update": "2023-02-26T10:02:03Z", + "ignition_total_time": 0, + "state": { + "name": "nodata", + "start": "2023-02-26T09:33:37Z", + "duration": 74289, + "debug_info": { + "boxId": 27953746, + "carId": 150662, + "msg": "OTHER", + "lastUpdate": 1677405723, + "lastValues": {"VOLTAGE": "14289"}, + }, + }, + "movement_state": { + "name": "nodata", + "start": "2023-02-26T09:33:37Z", + "duration": 75995, + }, + "fuel_type": "", + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "created_at": "2019-11-05T10:39:46Z", + "io_din": [ + {"no": 1, "label": "Auraus", "state": 1}, + {"no": 2, "label": "Hiekoitus", "state": 0}, + {"no": 3, "label": "Muu ty\u00f6", "state": 0}, + ], + }, + ] + assert num_elements <= len(units) + data = {"data": {"units": units[:num_elements]}} + return data From ef67c2fec0c6d9989f10d1f6c0e5443fe24f57ad Mon Sep 17 00:00:00 2001 From: juuso-j Date: Mon, 27 Feb 2023 10:38:41 +0200 Subject: [PATCH 158/188] Add Kuntec tests --- street_maintenance/tests/test_importers.py | 92 +++++++++++----------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/street_maintenance/tests/test_importers.py b/street_maintenance/tests/test_importers.py index 904384b9d..5d6d3e217 100644 --- a/street_maintenance/tests/test_importers.py +++ b/street_maintenance/tests/test_importers.py @@ -1,58 +1,58 @@ -from datetime import datetime from unittest.mock import patch import pytest -from street_maintenance.management.commands.constants import DATE_FORMATS, INFRAROAD +from street_maintenance.management.commands.constants import INFRAROAD from street_maintenance.models import MaintenanceUnit, MaintenanceWork +from .utils import ( + get_infraroad_units_json_data, + get_infraroad_works_json_data, + get_kuntec_units_json_data, + get_kuntec_works_json_data, +) -def get_infraroad_works_json_data(num_elements): - current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) - location_history = [ - { - "timestamp": f"{current_date} 08:29:49", - "coords": "(22.24957474 60.49515401)", - "events": ["au"], - }, - { - "timestamp": f"{current_date} 08:29:28", - "coords": "(22.24946401 60.49515848)", - "events": ["au"], - }, - { - "timestamp": f"{current_date} 08:28:32", - "coords": "(22.24944127 60.49519463)", - "events": ["hiekoitus"], - }, - ] - assert num_elements <= len(location_history) - data = {"location_history": location_history[:num_elements]} - return data +@pytest.mark.django_db +@patch("street_maintenance.management.commands.utils.get_json_data") +def test_kuntec( + get_json_data_mock, administrative_division, administrative_division_geometry +): + from street_maintenance.management.commands.utils import ( + create_kuntec_maintenance_units, + create_kuntec_maintenance_works, + ) -def get_infraroad_units_json_data(num_elements): - current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) - data = [ - { - "id": 2817625, - "last_location": { - "timestamp": f"{current_date} 06:31:34", - "coords": "(22.249642023816705 60.49569119699299)", - "events": ["au"], - }, - }, - { - "id": 12891825, - "last_location": { - "timestamp": f"{current_date} 08:29:49", - "coords": "(22.24957474 60.49515401)", - "events": ["Kenttien hoito"], - }, - }, - ] - assert num_elements <= len(data) - return data[:num_elements] + # Note, the fixture JSON contains one unit item with IO_DIN state 0(off) + # i.e., will not be included + get_json_data_mock.return_value = get_kuntec_units_json_data(2) + num_created_units, num_del_units = create_kuntec_maintenance_units() + assert num_created_units == 2 + assert num_del_units == 0 + assert MaintenanceUnit.objects.count() == 2 + unit = MaintenanceUnit.objects.first() + assert unit.unit_id == "150635" + assert unit.names == ["Auraus"] + get_json_data_mock.return_value = get_kuntec_units_json_data(1) + num_created_units, num_del_units = create_kuntec_maintenance_units() + assert unit.id == MaintenanceUnit.objects.first().id + assert num_created_units == 0 + assert num_del_units == 1 + assert MaintenanceUnit.objects.count() == 1 + get_json_data_mock.return_value = get_kuntec_works_json_data(2) + num_created_works, num_del_works = create_kuntec_maintenance_works(3) + assert num_created_works == 2 + assert num_del_works == 0 + assert MaintenanceWork.objects.count() == 2 + work = MaintenanceWork.objects.first() + work.events = ["auraus"] + work.original_event_names = ["Auraus"] + get_json_data_mock.return_value = get_kuntec_works_json_data(1) + num_created_works, num_del_works = create_kuntec_maintenance_works(3) + assert num_created_works == 0 + assert num_del_works == 1 + assert work.id == MaintenanceWork.objects.first().id + assert MaintenanceWork.objects.count() == 1 @pytest.mark.django_db From a7cc8fc60d80715e2774f8b800e87cb76dc7ebf3 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 08:44:51 +0200 Subject: [PATCH 159/188] Add YIT fixture functions --- street_maintenance/tests/utils.py | 119 ++++++++++++++++++++++++++++-- 1 file changed, 112 insertions(+), 7 deletions(-) diff --git a/street_maintenance/tests/utils.py b/street_maintenance/tests/utils.py index e48e54c15..d33308e17 100644 --- a/street_maintenance/tests/utils.py +++ b/street_maintenance/tests/utils.py @@ -3,10 +3,115 @@ from street_maintenance.management.commands.constants import ( DATE_FORMATS, INFRAROAD, + YIT, ) -def get_infraroad_works_json_data(num_elements): +def get_yit_vehicles_mock_data(num_elements): + data = [ + {"id": "82260ff7-589e-4cee-a8e0-124b615381f1", "vehicleTypeName": "Huoltoauto"}, + { + "id": "77396829-6275-4dbe-976f-f9d4f5254653", + "vehicleTypeName": "Kuorma-auto", + }, + ] + assert num_elements <= len(data) + return data[:num_elements] + + +def get_yit_event_types_mock_data(): + data = [ + { + "id": "a4f6188f-8c25-4f42-89be-f5a1bd7d833a", + "operationName": "Auraus ja sohjonpoisto", + }, + {"id": "a51e8e4c-8b16-4132-a882-70f6624c1f2b", "operationName": "Suolaus"}, + ] + return data + + +def get_yit_contract_mock_data(): + return "d73447e6-df70-4f4a-817d-3387b58aca6c" + + +def get_yit_routes_mock_data(num_elements): + current_date = datetime.now().date().strftime(DATE_FORMATS[YIT]) + data = [ + { + # Note, the MaintenanceUnit is retrieved by the vehicleType + "vehicleType": "82260ff7-589e-4cee-a8e0-124b615381f1", + "length": 21.0, + "geography": { + "crs": None, + "features": [ + { + "geometry": { + "coordinates": [ + [22.308685, 60.471465], + [22.308758333333337, 60.47151166666666], + [22.308844999999998, 60.47154999999999], + [22.30887476486449, 60.471557320473416], + ], + "type": "LineString", + }, + "properties": { + "streetAddress": "Koroistenkaari, Turku", + "featureType": "StreetAddress", + }, + "type": "Feature", + } + ], + "type": "FeatureCollection", + }, + "created": f"{current_date}T11:50:58.6173037Z", + "updated": f"{current_date}T11:50:58.6173037Z", + "deleted": False, + "id": "aaee2c3b-4296-44b3-aba0-82b859d4eea8", + "user": "442a5ab2-d58c-4c22-bae2-bcf55327cde7", + "contract": "d73447e6-df70-4f4a-817d-3387b58aca6c", + "startTime": f"{current_date}T11:48:44.694Z", + "endTime": f"{current_date}T11:48:46.984Z", + "operations": ["a51e8e4c-8b16-4132-a882-70f6624c1f2b"], + }, + { + "vehicleType": "82260ff7-589e-4cee-a8e0-124b615381f1", + "length": 0.0, + "geography": { + "crs": None, + "features": [ + { + "geometry": { + "coordinates": [ + [22.315554108363656, 60.47901418729062], + [22.31555399713308, 60.47901429688299], + ], + "type": "LineString", + }, + "properties": { + "streetAddress": "Polttolaitoksenkatu 13, Turku", + "featureType": "StreetAddress", + }, + "type": "Feature", + } + ], + "type": "FeatureCollection", + }, + "created": f"{current_date}T11:52:00.5136066Z", + "updated": f"{current_date}T11:52:00.5136066Z", + "deleted": False, + "id": "9c566b34-2bb5-46b0-9c0a-99f53eada2d2", + "user": "442a5ab2-d58c-4c22-bae2-bcf55327cde7", + "contract": "d73447e6-df70-4f4a-817d-3387b58aca6c", + "startTime": f"{current_date}T11:50:21.708Z", + "endTime": f"{current_date}T11:50:21.709Z", + "operations": ["a51e8e4c-8b16-4132-a882-70f6624c1f2b"], + }, + ] + assert num_elements <= len(data) + return data[:num_elements] + + +def get_infraroad_works_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) location_history = [ { @@ -30,7 +135,7 @@ def get_infraroad_works_json_data(num_elements): return data -def get_infraroad_units_json_data(num_elements): +def get_infraroad_units_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) data = [ { @@ -54,7 +159,7 @@ def get_infraroad_units_json_data(num_elements): return data[:num_elements] -def get_kuntec_works_json_data(num_elements): +def get_kuntec_works_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) routes = [ { @@ -107,7 +212,7 @@ def get_kuntec_works_json_data(num_elements): return data -def get_kuntec_units_json_data(num_elements): +def get_kuntec_units_mock_data(num_elements): null = None units = [ { @@ -148,7 +253,7 @@ def get_kuntec_units_json_data(num_elements): "duration": 151159, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 "created_at": "2019-11-05T10:10:38Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, @@ -194,7 +299,7 @@ def get_kuntec_units_json_data(num_elements): "duration": 75995, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 "created_at": "2019-11-05T10:39:46Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, @@ -240,7 +345,7 @@ def get_kuntec_units_json_data(num_elements): "duration": 75995, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 "created_at": "2019-11-05T10:39:46Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, From 7998406dd8ba39a0d6ffef938ae80bdaab8a06b2 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 08:45:20 +0200 Subject: [PATCH 160/188] Add YIT tests --- street_maintenance/tests/test_importers.py | 115 ++++++++++++++++++--- 1 file changed, 99 insertions(+), 16 deletions(-) diff --git a/street_maintenance/tests/test_importers.py b/street_maintenance/tests/test_importers.py index 5d6d3e217..4b0b09f8f 100644 --- a/street_maintenance/tests/test_importers.py +++ b/street_maintenance/tests/test_importers.py @@ -6,13 +6,92 @@ from street_maintenance.models import MaintenanceUnit, MaintenanceWork from .utils import ( - get_infraroad_units_json_data, - get_infraroad_works_json_data, - get_kuntec_units_json_data, - get_kuntec_works_json_data, + get_infraroad_units_mock_data, + get_infraroad_works_mock_data, + get_kuntec_units_mock_data, + get_kuntec_works_mock_data, + get_yit_contract_mock_data, + get_yit_event_types_mock_data, + get_yit_routes_mock_data, + get_yit_vehicles_mock_data, ) +@pytest.mark.django_db +@patch("street_maintenance.management.commands.utils.get_yit_vehicles") +def test_yit_units( + get_yit_vehicles_mock, + administrative_division, + administrative_division_geometry, +): + from street_maintenance.management.commands.utils import ( + create_yit_maintenance_units, + ) + + get_yit_vehicles_mock.return_value = get_yit_vehicles_mock_data(2) + num_created_units, num_del_units = create_yit_maintenance_units("test_access_token") + assert MaintenanceUnit.objects.count() == 2 + assert num_created_units == 2 + assert num_del_units == 0 + unit = MaintenanceUnit.objects.first() + unit_id = unit.id + assert unit.names == ["Huoltoauto"] + assert unit.unit_id == "82260ff7-589e-4cee-a8e0-124b615381f1" + get_yit_vehicles_mock.return_value = get_yit_vehicles_mock_data(1) + num_created_units, num_del_units = create_yit_maintenance_units("test_access_token") + assert unit.id == unit_id + assert num_created_units == 0 + assert num_del_units == 1 + assert MaintenanceUnit.objects.count() == 1 + + +@pytest.mark.django_db +@patch( + "street_maintenance.management.commands.utils.get_yit_vehicles", + return_value=get_yit_vehicles_mock_data(2), +) +@patch( + "street_maintenance.management.commands.utils.get_yit_contract", + return_value=get_yit_contract_mock_data(), +) +@patch( + "street_maintenance.management.commands.utils.get_yit_event_types", + return_value=get_yit_event_types_mock_data(), +) +@patch("street_maintenance.management.commands.utils.get_yit_routes") +def test_yit_works( + get_yit_routes_mock, + get_yit_vechiles_mock, + administrative_division, + administrative_division_geometry, +): + from street_maintenance.management.commands.utils import ( + create_yit_maintenance_units, + create_yit_maintenance_works, + ) + + create_yit_maintenance_units("test_access_token") + get_yit_routes_mock.return_value = get_yit_routes_mock_data(2) + num_created_works, num_del_works = create_yit_maintenance_works( + "test_access_token", 3 + ) + assert num_created_works == 2 + assert num_del_works == 0 + assert MaintenanceWork.objects.count() == 2 + work = MaintenanceWork.objects.first() + work_id = work.id + assert work.events == ["liukkaudentorjunta"] + assert work.original_event_names == ["Suolaus"] + get_yit_routes_mock.return_value = get_yit_routes_mock_data(1) + num_created_works, num_del_works = create_yit_maintenance_works( + "test_access_token", 3 + ) + assert num_created_works == 0 + assert num_del_works == 1 + assert work.id == work_id + assert MaintenanceWork.objects.count() == 1 + + @pytest.mark.django_db @patch("street_maintenance.management.commands.utils.get_json_data") def test_kuntec( @@ -25,33 +104,35 @@ def test_kuntec( # Note, the fixture JSON contains one unit item with IO_DIN state 0(off) # i.e., will not be included - get_json_data_mock.return_value = get_kuntec_units_json_data(2) + get_json_data_mock.return_value = get_kuntec_units_mock_data(2) num_created_units, num_del_units = create_kuntec_maintenance_units() assert num_created_units == 2 assert num_del_units == 0 assert MaintenanceUnit.objects.count() == 2 unit = MaintenanceUnit.objects.first() + unit_id = unit.id assert unit.unit_id == "150635" assert unit.names == ["Auraus"] - get_json_data_mock.return_value = get_kuntec_units_json_data(1) + get_json_data_mock.return_value = get_kuntec_units_mock_data(1) num_created_units, num_del_units = create_kuntec_maintenance_units() - assert unit.id == MaintenanceUnit.objects.first().id + assert unit.id == unit_id assert num_created_units == 0 assert num_del_units == 1 assert MaintenanceUnit.objects.count() == 1 - get_json_data_mock.return_value = get_kuntec_works_json_data(2) + get_json_data_mock.return_value = get_kuntec_works_mock_data(2) num_created_works, num_del_works = create_kuntec_maintenance_works(3) assert num_created_works == 2 assert num_del_works == 0 assert MaintenanceWork.objects.count() == 2 work = MaintenanceWork.objects.first() + work_id = work.id work.events = ["auraus"] work.original_event_names = ["Auraus"] - get_json_data_mock.return_value = get_kuntec_works_json_data(1) + get_json_data_mock.return_value = get_kuntec_works_mock_data(1) num_created_works, num_del_works = create_kuntec_maintenance_works(3) assert num_created_works == 0 assert num_del_works == 1 - assert work.id == MaintenanceWork.objects.first().id + assert work.id == work_id assert MaintenanceWork.objects.count() == 1 @@ -66,31 +147,33 @@ def test_infraroad( ) # Test unit creation - get_json_data_mock.return_value = get_infraroad_units_json_data(2) + get_json_data_mock.return_value = get_infraroad_units_mock_data(2) num_created_units, num_del_units = create_maintenance_units(INFRAROAD) assert MaintenanceUnit.objects.count() == 2 assert num_created_units == 2 assert num_del_units == 0 unit = MaintenanceUnit.objects.first() + unit_id = unit.id unit.unit_id = "2817625" unit.names = ["au"] - get_json_data_mock.return_value = get_infraroad_units_json_data(1) + get_json_data_mock.return_value = get_infraroad_units_mock_data(1) num_created_units, num_del_units = create_maintenance_units(INFRAROAD) - assert unit.id == MaintenanceUnit.objects.first().id + assert unit.id == unit_id assert num_created_units == 0 assert num_del_units == 1 assert MaintenanceUnit.objects.count() == 1 - get_json_data_mock.return_value = get_infraroad_works_json_data(3) + get_json_data_mock.return_value = get_infraroad_works_mock_data(3) num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) assert num_created_works == 3 assert num_del_works == 0 assert MaintenanceWork.objects.count() == 3 work = MaintenanceWork.objects.first() + work_id = work.id work.events = ["auraus"] work.original_event_names = ["au"] - get_json_data_mock.return_value = get_infraroad_works_json_data(1) + get_json_data_mock.return_value = get_infraroad_works_mock_data(1) num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) assert num_created_works == 0 assert num_del_works == 2 - assert work.id == MaintenanceWork.objects.first().id + assert work.id == work_id assert MaintenanceWork.objects.count() == 1 From a777042cc0e119a0763caad9d0c6632727654e31 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 08:46:11 +0200 Subject: [PATCH 161/188] Create function to get YIT vehicles --- street_maintenance/management/commands/utils.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index 8d969dae3..f3f381257 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -256,6 +256,7 @@ def create_yit_maintenance_works(access_token, history_size): f"Route contains multiple features. {route['geography']['features']}" ) coordinates = route["geography"]["features"][0]["geometry"]["coordinates"] + if is_nested_coordinates(coordinates) and len(coordinates) > 1: geometry = LineString(coordinates, srid=DEFAULT_SRID) else: @@ -533,8 +534,7 @@ def create_kuntec_maintenance_units(): return num_created, len(objs_to_delete) -@db.transaction.atomic -def create_yit_maintenance_units(access_token): +def get_yit_vehicles(access_token): response = requests.get( URLS[YIT][VEHICLES], headers={"Authorization": f"Bearer {access_token}"} ) @@ -543,11 +543,17 @@ def create_yit_maintenance_units(access_token): ), " Fetching YIT vehicles {} failed, status code: {}".format( URLS[YIT][VEHICLES], response.status_code ) + response.json() + + +@db.transaction.atomic +def create_yit_maintenance_units(access_token): + vehicles = get_yit_vehicles(access_token) num_created = 0 objs_to_delete = list( MaintenanceUnit.objects.filter(provider=YIT).values_list("id", flat=True) ) - for unit in response.json(): + for unit in vehicles: names = [unit["vehicleTypeName"]] obj, created = MaintenanceUnit.objects.get_or_create( unit_id=unit["id"], names=names, provider=YIT From 7af1fa34cad83326cd380a7a57318d69741d9a3b Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 10:08:40 +0200 Subject: [PATCH 162/188] Add multiple events, change order of YIT routes --- street_maintenance/tests/utils.py | 55 ++++++++++++++++--------------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/street_maintenance/tests/utils.py b/street_maintenance/tests/utils.py index d33308e17..6e9725d63 100644 --- a/street_maintenance/tests/utils.py +++ b/street_maintenance/tests/utils.py @@ -38,24 +38,21 @@ def get_yit_routes_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[YIT]) data = [ { - # Note, the MaintenanceUnit is retrieved by the vehicleType "vehicleType": "82260ff7-589e-4cee-a8e0-124b615381f1", - "length": 21.0, + "length": 0.0, "geography": { "crs": None, "features": [ { "geometry": { "coordinates": [ - [22.308685, 60.471465], - [22.308758333333337, 60.47151166666666], - [22.308844999999998, 60.47154999999999], - [22.30887476486449, 60.471557320473416], + [22.315554108363656, 60.47901418729062], + [22.31555399713308, 60.47901429688299], ], "type": "LineString", }, "properties": { - "streetAddress": "Koroistenkaari, Turku", + "streetAddress": "Polttolaitoksenkatu 13, Turku", "featureType": "StreetAddress", }, "type": "Feature", @@ -63,32 +60,35 @@ def get_yit_routes_mock_data(num_elements): ], "type": "FeatureCollection", }, - "created": f"{current_date}T11:50:58.6173037Z", - "updated": f"{current_date}T11:50:58.6173037Z", + "created": f"{current_date}T11:52:00.5136066Z", + "updated": f"{current_date}T11:52:00.5136066Z", "deleted": False, - "id": "aaee2c3b-4296-44b3-aba0-82b859d4eea8", + "id": "9c566b34-2bb5-46b0-9c0a-99f53eada2d2", "user": "442a5ab2-d58c-4c22-bae2-bcf55327cde7", "contract": "d73447e6-df70-4f4a-817d-3387b58aca6c", - "startTime": f"{current_date}T11:48:44.694Z", - "endTime": f"{current_date}T11:48:46.984Z", + "startTime": f"{current_date}T11:50:21.708Z", + "endTime": f"{current_date}T11:50:21.709Z", "operations": ["a51e8e4c-8b16-4132-a882-70f6624c1f2b"], }, { + # Note, the MaintenanceUnit is retrieved by the vehicleType "vehicleType": "82260ff7-589e-4cee-a8e0-124b615381f1", - "length": 0.0, + "length": 21.0, "geography": { "crs": None, "features": [ { "geometry": { "coordinates": [ - [22.315554108363656, 60.47901418729062], - [22.31555399713308, 60.47901429688299], + [22.308685, 60.471465], + [22.308758333333337, 60.47151166666666], + [22.308844999999998, 60.47154999999999], + [22.30887476486449, 60.471557320473416], ], "type": "LineString", }, "properties": { - "streetAddress": "Polttolaitoksenkatu 13, Turku", + "streetAddress": "Koroistenkaari, Turku", "featureType": "StreetAddress", }, "type": "Feature", @@ -96,14 +96,14 @@ def get_yit_routes_mock_data(num_elements): ], "type": "FeatureCollection", }, - "created": f"{current_date}T11:52:00.5136066Z", - "updated": f"{current_date}T11:52:00.5136066Z", + "created": f"{current_date}T11:50:58.6173037Z", + "updated": f"{current_date}T11:50:58.6173037Z", "deleted": False, - "id": "9c566b34-2bb5-46b0-9c0a-99f53eada2d2", + "id": "aaee2c3b-4296-44b3-aba0-82b859d4eea8", "user": "442a5ab2-d58c-4c22-bae2-bcf55327cde7", "contract": "d73447e6-df70-4f4a-817d-3387b58aca6c", - "startTime": f"{current_date}T11:50:21.708Z", - "endTime": f"{current_date}T11:50:21.709Z", + "startTime": f"{current_date}T11:48:44.694Z", + "endTime": f"{current_date}T11:48:46.984Z", "operations": ["a51e8e4c-8b16-4132-a882-70f6624c1f2b"], }, ] @@ -111,7 +111,8 @@ def get_yit_routes_mock_data(num_elements): return data[:num_elements] -def get_infraroad_works_mock_data(num_elements): +# Both Destia and Infraroad uses the fluentprogress API +def get_fluentprogress_works_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) location_history = [ { @@ -122,7 +123,7 @@ def get_infraroad_works_mock_data(num_elements): { "timestamp": f"{current_date} 08:29:28", "coords": "(22.24946401 60.49515848)", - "events": ["au"], + "events": ["au", "sivuaura", "sirotin"], }, { "timestamp": f"{current_date} 08:28:32", @@ -135,7 +136,7 @@ def get_infraroad_works_mock_data(num_elements): return data -def get_infraroad_units_mock_data(num_elements): +def get_fluentprogress_units_mock_data(num_elements): current_date = datetime.now().date().strftime(DATE_FORMATS[INFRAROAD]) data = [ { @@ -253,7 +254,7 @@ def get_kuntec_units_mock_data(num_elements): "duration": 151159, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l/100km"}, "created_at": "2019-11-05T10:10:38Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, @@ -299,7 +300,7 @@ def get_kuntec_units_mock_data(num_elements): "duration": 75995, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l/100km"}, "created_at": "2019-11-05T10:39:46Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, @@ -345,7 +346,7 @@ def get_kuntec_units_mock_data(num_elements): "duration": 75995, }, "fuel_type": "", - "avg_fuel_consumption": {"norm": 0, "measurement": "l\/100km"}, # noqa W605 + "avg_fuel_consumption": {"norm": 0, "measurement": "l/100km"}, "created_at": "2019-11-05T10:39:46Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, From a7be521ab0cf022afb9c2221514818cbdb49a5ff Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 10:09:27 +0200 Subject: [PATCH 163/188] Add Destia tests --- street_maintenance/tests/test_importers.py | 73 ++++++++++++++++++---- 1 file changed, 60 insertions(+), 13 deletions(-) diff --git a/street_maintenance/tests/test_importers.py b/street_maintenance/tests/test_importers.py index 4b0b09f8f..31f77a158 100644 --- a/street_maintenance/tests/test_importers.py +++ b/street_maintenance/tests/test_importers.py @@ -2,12 +2,12 @@ import pytest -from street_maintenance.management.commands.constants import INFRAROAD +from street_maintenance.management.commands.constants import DESTIA, INFRAROAD from street_maintenance.models import MaintenanceUnit, MaintenanceWork from .utils import ( - get_infraroad_units_mock_data, - get_infraroad_works_mock_data, + get_fluentprogress_units_mock_data, + get_fluentprogress_works_mock_data, get_kuntec_units_mock_data, get_kuntec_works_mock_data, get_yit_contract_mock_data, @@ -39,7 +39,7 @@ def test_yit_units( assert unit.unit_id == "82260ff7-589e-4cee-a8e0-124b615381f1" get_yit_vehicles_mock.return_value = get_yit_vehicles_mock_data(1) num_created_units, num_del_units = create_yit_maintenance_units("test_access_token") - assert unit.id == unit_id + assert unit_id == MaintenanceUnit.objects.first().id assert num_created_units == 0 assert num_del_units == 1 assert MaintenanceUnit.objects.count() == 1 @@ -88,7 +88,7 @@ def test_yit_works( ) assert num_created_works == 0 assert num_del_works == 1 - assert work.id == work_id + assert work_id == MaintenanceWork.objects.first().id assert MaintenanceWork.objects.count() == 1 @@ -115,7 +115,7 @@ def test_kuntec( assert unit.names == ["Auraus"] get_json_data_mock.return_value = get_kuntec_units_mock_data(1) num_created_units, num_del_units = create_kuntec_maintenance_units() - assert unit.id == unit_id + assert unit_id == MaintenanceUnit.objects.first().id assert num_created_units == 0 assert num_del_units == 1 assert MaintenanceUnit.objects.count() == 1 @@ -132,7 +132,7 @@ def test_kuntec( num_created_works, num_del_works = create_kuntec_maintenance_works(3) assert num_created_works == 0 assert num_del_works == 1 - assert work.id == work_id + assert work_id == MaintenanceWork.objects.first().id assert MaintenanceWork.objects.count() == 1 @@ -147,7 +147,7 @@ def test_infraroad( ) # Test unit creation - get_json_data_mock.return_value = get_infraroad_units_mock_data(2) + get_json_data_mock.return_value = get_fluentprogress_units_mock_data(2) num_created_units, num_del_units = create_maintenance_units(INFRAROAD) assert MaintenanceUnit.objects.count() == 2 assert num_created_units == 2 @@ -156,13 +156,13 @@ def test_infraroad( unit_id = unit.id unit.unit_id = "2817625" unit.names = ["au"] - get_json_data_mock.return_value = get_infraroad_units_mock_data(1) + get_json_data_mock.return_value = get_fluentprogress_units_mock_data(1) num_created_units, num_del_units = create_maintenance_units(INFRAROAD) - assert unit.id == unit_id + assert unit_id == MaintenanceUnit.objects.first().id assert num_created_units == 0 assert num_del_units == 1 assert MaintenanceUnit.objects.count() == 1 - get_json_data_mock.return_value = get_infraroad_works_mock_data(3) + get_json_data_mock.return_value = get_fluentprogress_works_mock_data(3) num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) assert num_created_works == 3 assert num_del_works == 0 @@ -171,9 +171,56 @@ def test_infraroad( work_id = work.id work.events = ["auraus"] work.original_event_names = ["au"] - get_json_data_mock.return_value = get_infraroad_works_mock_data(1) + get_json_data_mock.return_value = get_fluentprogress_works_mock_data(1) num_created_works, num_del_works = create_maintenance_works(INFRAROAD, 1, 10) assert num_created_works == 0 assert num_del_works == 2 - assert work.id == work_id + assert work_id == MaintenanceWork.objects.first().id + assert MaintenanceWork.objects.count() == 1 + + +@pytest.mark.django_db +@patch("street_maintenance.management.commands.utils.get_json_data") +def test_destia( + get_json_data_mock, administrative_division, administrative_division_geometry +): + from street_maintenance.management.commands.utils import ( + create_maintenance_units, + create_maintenance_works, + ) + + # Test unit creation + get_json_data_mock.return_value = get_fluentprogress_units_mock_data(2) + num_created_units, num_del_units = create_maintenance_units(DESTIA) + assert MaintenanceUnit.objects.count() == 2 + assert num_created_units == 2 + assert num_del_units == 0 + unit = MaintenanceUnit.objects.first() + unit_id = unit.id + unit.unit_id = "2817625" + unit.names = ["au"] + get_json_data_mock.return_value = get_fluentprogress_units_mock_data(1) + num_created_units, num_del_units = create_maintenance_units(DESTIA) + assert unit_id == MaintenanceUnit.objects.first().id + assert num_created_units == 0 + assert num_del_units == 1 + assert MaintenanceUnit.objects.count() == 1 + get_json_data_mock.return_value = get_fluentprogress_works_mock_data(3) + num_created_works, num_del_works = create_maintenance_works(DESTIA, 1, 10) + assert num_created_works == 3 + assert num_del_works == 0 + assert MaintenanceWork.objects.count() == 3 + work = MaintenanceWork.objects.first() + work_id = work.id + work.events = ["auraus"] + work.original_event_names = ["au"] + work = MaintenanceWork.objects.get( + original_event_names=["au", "sivuaura", "sirotin"] + ) + assert work.events == ["auraus", "auraus", "liukkaudentorjunta"] + get_json_data_mock.return_value = get_fluentprogress_works_mock_data(1) + num_created_works, num_del_works = create_maintenance_works(DESTIA, 1, 10) + assert num_created_works == 0 + assert num_del_works == 2 + assert work_id == MaintenanceWork.objects.first().id assert MaintenanceWork.objects.count() == 1 From 574126185e4759eccec330371719e9529ad62afe Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 10:13:39 +0200 Subject: [PATCH 164/188] Fix misplaced @pytest.mark.django_db --- street_maintenance/tests/conftest.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/street_maintenance/tests/conftest.py b/street_maintenance/tests/conftest.py index 0649403c2..f2b389397 100644 --- a/street_maintenance/tests/conftest.py +++ b/street_maintenance/tests/conftest.py @@ -49,7 +49,6 @@ def geometry_historys(): events=[AURAUS], ) geometry_historys.append(obj) - obj = GeometryHistory.objects.create( timestamp=now - timedelta(days=2), geometry=geometry, @@ -58,7 +57,6 @@ def geometry_historys(): events=[LIUKKAUDENTORJUNTA], ) geometry_historys.append(obj) - obj = GeometryHistory.objects.create( timestamp=now - timedelta(days=1), geometry=geometry, @@ -66,7 +64,6 @@ def geometry_historys(): provider=KUNTEC, events=[AURAUS], ) - geometry_historys.append(obj) obj = GeometryHistory.objects.create( timestamp=now - timedelta(days=2), @@ -75,9 +72,10 @@ def geometry_historys(): provider=KUNTEC, events=[AURAUS, LIUKKAUDENTORJUNTA], ) - geometry_historys.append(obj) @ pytest.mark.django_db - - + geometry_historys.append(obj) + + +@ pytest.mark.django_db @pytest.fixture def administrative_division_type(): adm_div_type = AdministrativeDivisionType.objects.create( From 5b270cccbe55ce7c2cf7d9275acd4bc34de7fd6f Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 10:21:56 +0200 Subject: [PATCH 165/188] Remove obsolete whitespace --- street_maintenance/tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/tests/conftest.py b/street_maintenance/tests/conftest.py index f2b389397..155080b23 100644 --- a/street_maintenance/tests/conftest.py +++ b/street_maintenance/tests/conftest.py @@ -75,7 +75,7 @@ def geometry_historys(): geometry_historys.append(obj) -@ pytest.mark.django_db +@pytest.mark.django_db @pytest.fixture def administrative_division_type(): adm_div_type = AdministrativeDivisionType.objects.create( From 9c7ec89a93b0fd900db4e4c82c9643b9ba6d7ddb Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 10:50:17 +0200 Subject: [PATCH 166/188] Format --- street_maintenance/tests/conftest.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/street_maintenance/tests/conftest.py b/street_maintenance/tests/conftest.py index 155080b23..cd4b5fdc2 100644 --- a/street_maintenance/tests/conftest.py +++ b/street_maintenance/tests/conftest.py @@ -72,9 +72,9 @@ def geometry_historys(): provider=KUNTEC, events=[AURAUS, LIUKKAUDENTORJUNTA], ) - geometry_historys.append(obj) - - + geometry_historys.append(obj) + + @pytest.mark.django_db @pytest.fixture def administrative_division_type(): From 03a8e729ef755d1ae471b2c0790c7f25558fd172 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 15:05:06 +0200 Subject: [PATCH 167/188] Change io_din state to 1 --- street_maintenance/tests/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/tests/utils.py b/street_maintenance/tests/utils.py index 6e9725d63..078807e51 100644 --- a/street_maintenance/tests/utils.py +++ b/street_maintenance/tests/utils.py @@ -258,7 +258,7 @@ def get_kuntec_units_mock_data(num_elements): "created_at": "2019-11-05T10:10:38Z", "io_din": [ {"no": 1, "label": "Auraus", "state": 1}, - {"no": 2, "label": "Hiekoitus", "state": 0}, + {"no": 2, "label": "Hiekoitus", "state": 1}, {"no": 3, "label": "Muu ty\u00f6", "state": 0}, ], }, From cf485baf54044d292b4c54541e3b2ef7501ca453 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 15:06:12 +0200 Subject: [PATCH 168/188] No duplicate events will be added --- street_maintenance/management/commands/utils.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index f3f381257..38816da95 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -170,7 +170,6 @@ def get_linestrings_from_points(objects, queryset, provider): prev_geometry = elem.geometry points.append(elem.geometry) prev_timestamp = elem.timestamp - if len(points) > 1: discarded_linestrings += add_geometry_history_objects( objects, points, elem, provider @@ -276,7 +275,8 @@ def create_yit_maintenance_works(access_token, history_size): for e in EVENT_MAPPINGS[event_name]: # If mapping value is None, the event is not used. if e: - events.append(e) + if e not in events: + events.append(e) original_event_names.append(event_name_mappings[operation]) else: logger.warning( @@ -342,7 +342,8 @@ def create_kuntec_maintenance_works(history_size): for e in EVENT_MAPPINGS[event_name]: # If mapping value is None, the event is not used. if e: - events.append(e) + if e not in events: + events.append(e) original_event_names.append(name) else: logger.warning(f"Found unmapped event: {event_name}") @@ -422,7 +423,8 @@ def create_maintenance_works(provider, history_size, fetch_size): for e in EVENT_MAPPINGS[event_name]: # If mapping value is None, the event is not used. if e: - events.append(e) + if e not in events: + events.append(e) original_event_names.append(event) else: logger.warning(f"Found unmapped event: {event}") From 7fa45f85092a74cf3720ee3a5f6b7c48c86640d6 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 28 Feb 2023 15:09:34 +0200 Subject: [PATCH 169/188] Add test to check that duplicate events are not created --- street_maintenance/tests/test_importers.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/street_maintenance/tests/test_importers.py b/street_maintenance/tests/test_importers.py index 31f77a158..ecfdbc9a3 100644 --- a/street_maintenance/tests/test_importers.py +++ b/street_maintenance/tests/test_importers.py @@ -112,7 +112,7 @@ def test_kuntec( unit = MaintenanceUnit.objects.first() unit_id = unit.id assert unit.unit_id == "150635" - assert unit.names == ["Auraus"] + assert unit.names == ["Auraus", "Hiekoitus"] get_json_data_mock.return_value = get_kuntec_units_mock_data(1) num_created_units, num_del_units = create_kuntec_maintenance_units() assert unit_id == MaintenanceUnit.objects.first().id @@ -126,8 +126,8 @@ def test_kuntec( assert MaintenanceWork.objects.count() == 2 work = MaintenanceWork.objects.first() work_id = work.id - work.events = ["auraus"] - work.original_event_names = ["Auraus"] + work.events = ["auraus", "liukkaudentorjunta"] + work.original_event_names = ["Auraus", "Hiekoitus"] get_json_data_mock.return_value = get_kuntec_works_mock_data(1) num_created_works, num_del_works = create_kuntec_maintenance_works(3) assert num_created_works == 0 @@ -217,7 +217,8 @@ def test_destia( work = MaintenanceWork.objects.get( original_event_names=["au", "sivuaura", "sirotin"] ) - assert work.events == ["auraus", "auraus", "liukkaudentorjunta"] + # Test that duplicate events are not included, as "sivuaura" and "au" are mapped to "auraus" + assert work.events == ["auraus", "liukkaudentorjunta"] get_json_data_mock.return_value = get_fluentprogress_works_mock_data(1) num_created_works, num_del_works = create_maintenance_works(DESTIA, 1, 10) assert num_created_works == 0 From aeeed06bf93f897b5703c93b9d0bff7f2f4a3d77 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Mar 2023 10:17:06 +0200 Subject: [PATCH 170/188] Add hours=2 to timedelta to ensure sufficient span --- street_maintenance/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/tests/test_api.py b/street_maintenance/tests/test_api.py index 61bc35483..e46fcd43c 100644 --- a/street_maintenance/tests/test_api.py +++ b/street_maintenance/tests/test_api.py @@ -48,7 +48,7 @@ def test_geometry_history(api_client, geometry_historys): geometry_history = response.json()["results"][0] assert geometry_history["geometry_type"] == "LineString" assert geometry_history["provider"] == INFRAROAD - start_date_time = datetime.now(UTC_TIMEZONE) - timedelta(days=1) + start_date_time = datetime.now(UTC_TIMEZONE) - timedelta(days=1, hours=2) url = ( reverse("street_maintenance:geometry_history-list") + f"?start_date_time={start_date_time.strftime(START_DATE_TIME_FORMAT)}" From ab58e7e29f822d8c687d6b20e3d7f6ccea0f7780 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Mar 2023 10:58:37 +0200 Subject: [PATCH 171/188] Update information of importing to reflect importer changes --- street_maintenance/README.md | 54 +++++++++++++++--------------------- 1 file changed, 22 insertions(+), 32 deletions(-) diff --git a/street_maintenance/README.md b/street_maintenance/README.md index c819906a9..2d7eccc26 100644 --- a/street_maintenance/README.md +++ b/street_maintenance/README.md @@ -1,53 +1,43 @@ -# Street Maintance +# Street Maintance history Django app for importing and serving street maintenance data. -## Importers +## Importer +Name: +import_street_maintenance_history -### Infraroad -``` -./manage.py import_infraroad_street_maintenance_history -``` -### YIT -``` -./manage.py import_yit_street_maintenance_history -``` -### Kuntec +Providers: +* YIT +* KUNTEC +* INFRAROAD +* DESTIA + +Parameters: +* --providers, list of providers to import, e.g., --provider yit kuntec +* --history-size, the number of days to import (default is 4 and max is 31) +* --fetch-size (only available for infraroad and destia), the number of works to import per unit(default is 10000). + +### Examples: +To import DESTIA street maintenance history with history size 2: ``` -./manage.py import_kuntec_street_maintenance_history +./manage.py import_street_maintenance_history --providers destia --history-size 2 ``` - -### Destia +To import KUNTEC and INFRAROAD street maintenance history: ``` -./manage.py import_destia_street_maintenance_history +./manage.py import_street_maintenance_history --providers kuntec infraroad ``` +Note, only the MaintenanceWorks and MaintenanceUnits for the given provider from the latest import are stored and the rest are deleted. The GeometryHistory is generated only if more than one MaintenanceWork is created. ### Periodically imorting To periodically import data use Celery, for more information [see](https://github.com/City-of-Turku/smbackend/wiki/Celery-Tasks#street-maintenance-history-street_maintenancetasksimport_street_maintenance_history). -### Deleting street maintenance history for a provider +## Deleting street maintenance history for a provider It is possible to delete street maintenance history for a provider. e.g., to delete all street maintenance history for provider 'destia': ``` ./manage.py delete_street_maintenance_history destia ``` -## History sizes -To set the history size use the '--history-size' parameter and give the value as argument. -e.g., would import the Autori data for the last 30 days. -``` -./manage.py import_yit_street_maintenance_history --history-size 30 -``` -### Infraroad -The default history size for a infraroad maintenance unit is 10000. That is works per unit. A work contains the timestamp, point data and events. -### Destia -The default history size for a Destia maintenance unit is 10000. -### Yit -The history size is in days. The default is 5. -Note, the max size for Autori history is 31 days. -### Kuntec -The history size is in days. The default is 5. -Note, the max size for Kuntec history is 31 days. ## API See: specificatin.swagger.yaml \ No newline at end of file From 1413988c16367dbb97c2f7d999edb4fa4d0c4663 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Mar 2023 11:10:45 +0200 Subject: [PATCH 172/188] Fix typo --- street_maintenance/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/README.md b/street_maintenance/README.md index 2d7eccc26..adc27f18e 100644 --- a/street_maintenance/README.md +++ b/street_maintenance/README.md @@ -1,4 +1,4 @@ -# Street Maintance history +# Street Maintenance history Django app for importing and serving street maintenance data. From 6a8070c158ec4fb4bc0830865c287bd386017442 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Wed, 1 Mar 2023 14:14:20 +0200 Subject: [PATCH 173/188] Update descriptions, add example time format and time constants --- street_maintenance/api/views.py | 34 +++++++++++++++++++-------------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/street_maintenance/api/views.py b/street_maintenance/api/views.py index fea6ecba8..0110d766d 100644 --- a/street_maintenance/api/views.py +++ b/street_maintenance/api/views.py @@ -20,15 +20,14 @@ from street_maintenance.models import GeometryHistory, MaintenanceUnit, MaintenanceWork UTC_TIMEZONE = pytz.timezone("UTC") - -# Default is 3minutes 3*60s -DEFAULT_MAX_WORK_LENGTH = 180 +EXAMPLE_TIME_FORMAT = "YYYY-MM-DD HH:MM:SS" +EXAMPLE_TIME = "2022-09-18 10:00:00" EVENT_PARAM = OpenApiParameter( name="event", location=OpenApiParameter.QUERY, description=( "Return objects of given event. " - 'Event choices are: "auraus", "liukkaudentorjunta", "hiekanpoisto", "puhtaanapito", ' + 'Event choices are: " ,".join(PROVIDERS).lower(), ' 'E.g. "auraus".' ), required=False, @@ -46,7 +45,7 @@ location=OpenApiParameter.QUERY, description=( "Get objects with timestamp newer than the start_date_time. " - 'The format for the timestamp is: YYYY-MM-DD HH:MM:SS, e.g. "2022-09-18 10:00:00".' + f'The format for the timestamp is: {EXAMPLE_TIME_FORMAT}, e.g. "{EXAMPLE_TIME}".' ), required=False, type=str, @@ -77,9 +76,11 @@ class ActiveEventsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): @extend_schema_view( list=extend_schema( parameters=_maintenance_works_list_parameters, - description="A maintenance work is a single work performed by a provider. The geometry can be a point or a " - "linestring. Note, if the geometry is a point, the latitude and longitude will be separately serialized." - " If the geometry is a linestring a separate list of coordinates will be serialized.", + description="A MaintenanceWork object is a single work performed by a provider. The geometry can be a point " + "or a linestring. The geometry and timestamp are assigned directly from the source data. The " + "original_event_names contains the names of the events in the source data and the events field is the mapped " + "names. Note, if the geometry is a point, the latitude and longitude will be separately serialized. If the " + "geometry is a linestring a separate list of coordinates will be serialized.", ) ) class MaintenanceWorkViewSet(viewsets.ReadOnlyModelViewSet): @@ -106,7 +107,7 @@ def get_queryset(self): ) except ValueError: raise ParseError( - "'start_date_time' must be in format YYYY-MM-DD HH:MM:SS elem.g.,'2022-09-18 10:00:00'" + f"'start_date_time' must be in format {EXAMPLE_TIME_FORMAT} elem.g.,'{EXAMPLE_TIME}'" ) start_date_time = start_date_time.replace(tzinfo=UTC_TIMEZONE) queryset = queryset.filter(timestamp__gte=start_date_time) @@ -126,8 +127,10 @@ def list(self, request): @extend_schema_view( list=extend_schema( - description="Maintanance units from where the works are derived.", - ) + description="MaintananceUnit objets are the entities that creates the MaintenanceWorks. Every MaintenanceWork " + "has a relation to a MaintenanceUnit. The type of the MaintenanceUnit can vary depending on the provider. It " + "can be a machine or a event", + ), ) class MaintenanceUnitViewSet(viewsets.ReadOnlyModelViewSet): queryset = MaintenanceUnit.objects.all() @@ -144,8 +147,11 @@ class MaintenanceUnitViewSet(viewsets.ReadOnlyModelViewSet): @extend_schema_view( list=extend_schema( parameters=_geometry_history_list_parameters, - description="Returns objects where geometries are precalculated/processed from point data or linestrings." - "The coordinates are in SRID 4326.", + description="GeometryHistory objects are processed from MaintenanceWork objects. MaintenanceWorks with " + "linestrings are validated and if valid a GeometryHistory object is created with the linestring geometry. " + "From MaintenanceWork objects that cointains point data linestrings are generated. The linestrings are " + "generated by comparing the timestamp, event, distance and provider. For every valid generated linestring a " + "GeometryHistory object is created. The coordinates are in SRID 4326.", ) ) class GeometryHitoryViewSet(viewsets.ReadOnlyModelViewSet): @@ -173,7 +179,7 @@ def get_queryset(self): ) except ValueError: raise ParseError( - "'start_date_time' must be in format YYYY-MM-DD HH:MM:SS elem.g.,'2022-09-18 10:00:00'" + f"'start_date_time' must be in format {EXAMPLE_TIME_FORMAT} elem.g.,'{EXAMPLE_TIME}'" ) start_date_time = start_date_time.replace(tzinfo=UTC_TIMEZONE) queryset = queryset.filter(timestamp__gte=start_date_time) From f6911b4461aa68b802d14414108fc1a90bfd237d Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 10:51:06 +0200 Subject: [PATCH 174/188] Delete MobileUnit with given content types name --- mobility_data/importers/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/importers/utils.py b/mobility_data/importers/utils.py index bfed1c31b..c49ba1dd4 100644 --- a/mobility_data/importers/utils.py +++ b/mobility_data/importers/utils.py @@ -68,7 +68,7 @@ def fetch_json(url): def delete_mobile_units(name): - ContentType.objects.filter(name=name).delete() + MobileUnit.objects.filter(content_types__name=name).delete() def create_mobile_unit_as_unit_reference(unit_id, content_type): From fcab3517122f50b24c9decc2b575078dcd04dd01 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:49:25 +0200 Subject: [PATCH 175/188] Serialize services_unit instances --- mobility_data/api/serializers/mobile_unit.py | 42 +++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/mobility_data/api/serializers/mobile_unit.py b/mobility_data/api/serializers/mobile_unit.py index f4ebe60e4..fbc5ef660 100644 --- a/mobility_data/api/serializers/mobile_unit.py +++ b/mobility_data/api/serializers/mobile_unit.py @@ -44,6 +44,7 @@ class MobileUnitSerializer(serializers.ModelSerializer): content_types = ContentTypeSerializer(many=True, read_only=True) mobile_unit_group = MobileUnitGroupBasicInfoSerializer(many=False, read_only=True) + # geometry = serializers.SerializerMethodField(read_only=True) geometry_coords = serializers.SerializerMethodField(read_only=True) class Meta: @@ -85,10 +86,18 @@ class Meta: def to_representation(self, obj): representation = super().to_representation(obj) - # If mobile_unit has a unit_id we serialize the data from the services_unit table. - unit_id = obj.unit_id - if unit_id: - unit = Unit.objects.get(id=unit_id) + unit_id = getattr(obj, "unit_id", None) + # If serializing Unit instance or MobileUnit with unit_id. + if self.context.get("services_unit_instance", False) or unit_id: + if unit_id: + # When serializing the MobileUnit from the retrieve method + try: + unit = Unit.objects.get(id=unit_id) + except Unit.DoesNotExist: + return representation + else: + # The obj is a Unit instance. + unit = obj for field in self.fields: # lookup the field name in the service_unit table, as not all field names that contains # similar data has the same name. @@ -100,21 +109,34 @@ def to_representation(self, obj): if hasattr(unit, key): # unit.municipality is of type munigeo.models.Municipality and not serializable if key == "municipality": - representation[field] = unit.municipality.id + muni = getattr(unit, key, None) + representation[field] = muni.id if muni else None else: - representation[field] = getattr(unit, key) + representation[field] = getattr(unit, key, None) # Serialize the MobileUnit id, otherwise would serialize the serivce_unit id. if field == "id": - representation["id"] = obj.id + try: + representation["id"] = MobileUnit.objects.get( + unit_id=unit.id + ).id + except MobileUnit.DoesNotExist: + representation["id"] = unit.id # The location field must be serialized with its wkt value. if unit.location: representation["geometry"] = unit.location.wkt return representation def get_geometry_coords(self, obj): - # If stored to Unit table, retrieve geometry from there. - if obj.unit_id: - geometry = Unit.objects.get(id=obj.unit_id).location + unit_id = getattr(obj, "unit_id", None) + if unit_id: + # If stored to Unit table, retrieve geometry from there. + try: + geometry = Unit.objects.get(id=unit_id).location + except Unit.DoesNotExist: + return None + # If serializing Unit object retrieved from the view. + elif self.context.get("services_unit_instance", False): + geometry = obj.location else: geometry = obj.geometry if isinstance(geometry, GEOSGeometry): From 6d30d297230b4534b4041521cf7f39f8c5c3c06e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:51:35 +0200 Subject: [PATCH 176/188] Retrieve and filter services_unit instances if unit_id set --- mobility_data/api/views.py | 44 ++++++++++++++++++++++++++++++-------- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/mobility_data/api/views.py b/mobility_data/api/views.py index ba554115f..580710ed7 100644 --- a/mobility_data/api/views.py +++ b/mobility_data/api/views.py @@ -1,3 +1,4 @@ +import types from distutils.util import strtobool from django.contrib.gis.gdal import SpatialReference @@ -8,6 +9,8 @@ from rest_framework.exceptions import ParseError from rest_framework.response import Response +from services.models import Unit + from ..models import ContentType, GroupType, MobileUnit, MobileUnitGroup from .serializers import ( ContentTypeSerializer, @@ -17,6 +20,10 @@ MobileUnitSerializer, ) +FIELD_TYPES = types.SimpleNamespace() +FIELD_TYPES.FLOAT = float +FIELD_TYPES.INT = int +FIELD_TYPES.BOOL = bool # Mappings, so that deprecated type_names will work. # These will be removed when the front end is updated. group_name_mappings = {"CRE": "CultureRoute"} @@ -179,6 +186,7 @@ def list(self, request): and transforms to given srid. """ queryset = None + unit_ids = [] filters = self.request.query_params srid, latlon = get_srid_and_latlon(filters) if "type_name" in filters: @@ -191,15 +199,25 @@ def list(self, request): "type_name does not exist.", status=status.HTTP_400_BAD_REQUEST ) queryset = MobileUnit.objects.filter(content_types__name=type_name) + # If the data locates in the services_unit table (i.e., MobileUnit has a unit_id) + # get the unit_ids to retrieve the Units for filtering(bbox and extra) + unit_ids = list( + queryset.filter(unit_id__isnull=False).values_list("unit_id", flat=True) + ) else: queryset = MobileUnit.objects.all() + services_unit_instances = True if len(unit_ids) > 0 else False + if services_unit_instances: + queryset = Unit.objects.filter(id__in=unit_ids) + if "bbox" in filters: val = filters.get("bbox", None) + geometry_field_name = "location" if services_unit_instances else "geometry" if val: ref = SpatialReference(filters.get("bbox_srid", 4326)) bbox_geometry_filter = munigeo_api.build_bbox_filter( - ref, val, "geometry" + ref, val, geometry_field_name ) queryset = queryset.filter(Q(**bbox_geometry_filter)) @@ -225,18 +243,26 @@ def list(self, request): status=status.HTTP_400_BAD_REQUEST, ) - if field_type == float: - value = float(value) - elif field_type == int: - value = int(value) - elif field_type == bool: - value = strtobool(value) - value = bool(value) + match field_type: + case FIELD_TYPES.FLOAT: + value = float(value) + case FIELD_TYPES.INT: + value = int(value) + case FIELD_TYPES.BOOL: + value = strtobool(value) + value = bool(value) + queryset = queryset.filter(**{filter: value}) page = self.paginate_queryset(queryset) serializer = MobileUnitSerializer( - page, many=True, context={"srid": srid, "latlon": latlon} + page, + many=True, + context={ + "srid": srid, + "latlon": latlon, + "services_unit_instances": services_unit_instances, + }, ) return self.get_paginated_response(serializer.data) From f017ce3a753f33e59a6dd43518752cf12dd3dc87 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:52:40 +0200 Subject: [PATCH 177/188] Test serialization from services_unit --- mobility_data/tests/test_api.py | 36 +++++++++++++++++++++++++++++---- 1 file changed, 32 insertions(+), 4 deletions(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index e9123f58c..70c855c71 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -9,6 +9,7 @@ def test_content_type(api_client, content_types): url = reverse("mobility_data:content_types-list") response = api_client.get(url) assert response.status_code == 200 + assert len(response.json()["results"]) == len(content_types) result = response.json()["results"][0] assert result["name"] == "Test" assert result["description"] == "test content type" @@ -25,11 +26,18 @@ def test_group_type(api_client, group_type): @pytest.mark.django_db -def test_mobile_unit(api_client, mobile_units, content_types): +def test_mobile_unit(api_client, mobile_units, content_types, unit): url = reverse("mobility_data:mobile_units-list") response = api_client.get(url) assert response.status_code == 200 - result = response.json()["results"][1] + assert len(response.json()["results"]) == len(mobile_units) + url = reverse( + "mobility_data:mobile_units-detail", + args=["aa6c2903-d36f-4c61-b828-19084fc7a64b"], + ) + response = api_client.get(url) + assert response.status_code == 200 + result = response.json() assert result["name"] == "Test mobileunit" assert result["description"] == "Test description" assert result["content_types"][0]["id"] == str(content_types[0].id) @@ -39,8 +47,14 @@ def test_mobile_unit(api_client, mobile_units, content_types): assert result["geometry"] == Point( 235404.6706163187, 6694437.919005549, srid=settings.DEFAULT_SRID ) + url = reverse( + "mobility_data:mobile_units-detail", + args=["ba6c2903-d36f-4c61-b828-19084fc7a64b"], + ) + response = api_client.get(url) + assert response.status_code == 200 # Test multiple content types - result = response.json()["results"][0] + result = response.json() assert len(result["content_types"]) == 2 assert result["content_types"][0]["name"] == "Test" assert result["content_types"][1]["name"] == "Test2" @@ -91,10 +105,24 @@ def test_mobile_unit(api_client, mobile_units, content_types): url = reverse("mobility_data:mobile_units-list") + "?bbox=22.1,60.2,2.3,60.4" response = api_client.get(url) assert len(response.json()["results"]) == 0 + # Test data serialization from services_unit model + url = reverse( + "mobility_data:mobile_units-detail", + args=["ca6c2903-d36f-4c61-b828-19084fc7a64b"], + ) + response = api_client.get(url) + assert response.status_code == 200 + result = response.json() + assert result["name"] == "Test unit" + assert result["description"] == "desc" + assert result["content_types"][0]["name"] == "Test unit" + assert result["geometry"] == "POINT (24.24 62.22)" + assert result["geometry_coords"]["lon"] == 24.24 + assert result["geometry_coords"]["lat"] == 62.22 @pytest.mark.django_db -def test_mobile_unit_group(api_client, mobile_unit_group, group_type): +def test_mobile_unit_group(api_client, mobile_unit_group, group_type, unit): url = reverse("mobility_data:mobile_unit_groups-list") response = api_client.get(url) assert response.status_code == 200 From 7a1191eedb914cc91de9d4dec6d6e5d5d25c9275 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:53:25 +0200 Subject: [PATCH 178/188] Add service, unit and mobile_unit with unit_id fixture --- mobility_data/tests/conftest.py | 53 ++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/mobility_data/tests/conftest.py b/mobility_data/tests/conftest.py index 8105bff3c..a344ab0de 100644 --- a/mobility_data/tests/conftest.py +++ b/mobility_data/tests/conftest.py @@ -1,6 +1,7 @@ import pytest from django.conf import settings from django.contrib.gis.geos import GEOSGeometry, Point +from django.utils import timezone from munigeo.models import ( Address, AdministrativeDivision, @@ -11,6 +12,8 @@ ) from rest_framework.test import APIClient +from services.models import Service, Unit, UnitServiceDetails + from ..models import ContentType, GroupType, MobileUnit, MobileUnitGroup # borders of Turku in well known text format. @@ -48,12 +51,26 @@ def api_client(): @pytest.fixture def content_types(): content_types = [ - ContentType.objects.create(name="Test", description="test content type") + ContentType.objects.create( + id="aa6c2903-d36f-4c61-b828-19084fc7a64b", + name="Test", + description="test content type", + ) ] content_types.append( - ContentType.objects.create(name="Test2", description="test content type2") + ContentType.objects.create( + id="ba6c2903-d36f-4c61-b828-19084fc7a64b", + name="Test2", + description="test content type2", + ) + ) + content_types.append( + ContentType.objects.create( + id="ca6c2903-d36f-4c61-b828-19084fc7a64b", + name="Test unit", + description="test content type3", + ) ) - return content_types @@ -79,6 +96,7 @@ def mobile_units(content_types): geometry = Point(22.21, 60.3, srid=4326) geometry.transform(settings.DEFAULT_SRID) mobile_unit = MobileUnit.objects.create( + id="aa6c2903-d36f-4c61-b828-19084fc7a64b", name="Test mobileunit", description="Test description", geometry=geometry, @@ -93,17 +111,44 @@ def mobile_units(content_types): "test_bool": True, } mobile_unit = MobileUnit.objects.create( + id="ba6c2903-d36f-4c61-b828-19084fc7a64b", name="Test2 mobileunit", description="Test2 description", - geometry=Point(43.43, 22.22, srid=settings.DEFAULT_SRID), + geometry=Point(23.43, 62.22, srid=settings.DEFAULT_SRID), extra=extra, ) mobile_unit.content_types.add(content_types[0]) mobile_unit.content_types.add(content_types[1]) mobile_units.append(mobile_units) + mobile_unit = MobileUnit.objects.create( + id="ca6c2903-d36f-4c61-b828-19084fc7a64b", unit_id=1 + ) + mobile_unit.content_types.add(content_types[2]) + mobile_units.append(mobile_units) return mobile_units +@pytest.mark.django_db +@pytest.fixture +def service(): + return Service.objects.create(id=1, name="test", last_modified_time=timezone.now()) + + +@pytest.mark.django_db +@pytest.fixture +def unit(service): + unit = Unit.objects.create( + id=1, + name="Test unit", + description="desc", + last_modified_time=timezone.now(), + provider_type=1, + location=Point(24.24, 62.22, srid=settings.DEFAULT_SRID), + ) + UnitServiceDetails(unit=unit, service=service).save() + return unit + + @pytest.mark.django_db @pytest.fixture def mobile_unit_group(group_type): From c83ac5aa4532352ccb1d8d54e5e6d70945fec22a Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:56:35 +0200 Subject: [PATCH 179/188] Change key to services_unit_instances --- mobility_data/api/serializers/mobile_unit.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mobility_data/api/serializers/mobile_unit.py b/mobility_data/api/serializers/mobile_unit.py index fbc5ef660..0b928a0ef 100644 --- a/mobility_data/api/serializers/mobile_unit.py +++ b/mobility_data/api/serializers/mobile_unit.py @@ -88,7 +88,7 @@ def to_representation(self, obj): representation = super().to_representation(obj) unit_id = getattr(obj, "unit_id", None) # If serializing Unit instance or MobileUnit with unit_id. - if self.context.get("services_unit_instance", False) or unit_id: + if self.context.get("services_unit_instances", False) or unit_id: if unit_id: # When serializing the MobileUnit from the retrieve method try: @@ -135,7 +135,7 @@ def get_geometry_coords(self, obj): except Unit.DoesNotExist: return None # If serializing Unit object retrieved from the view. - elif self.context.get("services_unit_instance", False): + elif self.context.get("services_unit_instances", False): geometry = obj.location else: geometry = obj.geometry From a2197e5cff3b31805f8f2566507683728af4f768 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 11:57:16 +0200 Subject: [PATCH 180/188] Fix typo --- .../management/commands/import_share_car_parking_places.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/management/commands/import_share_car_parking_places.py b/mobility_data/management/commands/import_share_car_parking_places.py index 68ade93f1..d0e003ffc 100644 --- a/mobility_data/management/commands/import_share_car_parking_places.py +++ b/mobility_data/management/commands/import_share_car_parking_places.py @@ -19,4 +19,4 @@ def handle(self, *args, **options): objects = get_car_share_parking_place_objects(geojson_file=geojson_file) save_to_database(objects) - logger.info(f"Imported {len(objects)} char share parking places.") + logger.info(f"Imported {len(objects)} car share parking places.") From 60cfbff90aed5fb89ff176cfc647bf1a3e8d3478 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 12:03:42 +0200 Subject: [PATCH 181/188] Add missing f-string and braces --- street_maintenance/api/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/api/views.py b/street_maintenance/api/views.py index 0110d766d..ffef8da58 100644 --- a/street_maintenance/api/views.py +++ b/street_maintenance/api/views.py @@ -27,7 +27,7 @@ location=OpenApiParameter.QUERY, description=( "Return objects of given event. " - 'Event choices are: " ,".join(PROVIDERS).lower(), ' + f'Event choices are: {", ".join(PROVIDERS).lower()}, ' 'E.g. "auraus".' ), required=False, From 37fcaf400b419a7a8ec0f4cc1870d479ebcb8de7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 12:25:29 +0200 Subject: [PATCH 182/188] Remove obsolete unit fixture --- mobility_data/tests/test_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobility_data/tests/test_api.py b/mobility_data/tests/test_api.py index 70c855c71..a8f20b136 100644 --- a/mobility_data/tests/test_api.py +++ b/mobility_data/tests/test_api.py @@ -122,7 +122,7 @@ def test_mobile_unit(api_client, mobile_units, content_types, unit): @pytest.mark.django_db -def test_mobile_unit_group(api_client, mobile_unit_group, group_type, unit): +def test_mobile_unit_group(api_client, mobile_unit_group, group_type): url = reverse("mobility_data:mobile_unit_groups-list") response = api_client.get(url) assert response.status_code == 200 From 4bb054d454eb55f58f319457d35bad860fe3c1c7 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Fri, 3 Mar 2023 14:22:32 +0200 Subject: [PATCH 183/188] Remove obsolete commented line --- mobility_data/api/serializers/mobile_unit.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mobility_data/api/serializers/mobile_unit.py b/mobility_data/api/serializers/mobile_unit.py index 0b928a0ef..6708eb514 100644 --- a/mobility_data/api/serializers/mobile_unit.py +++ b/mobility_data/api/serializers/mobile_unit.py @@ -44,7 +44,6 @@ class MobileUnitSerializer(serializers.ModelSerializer): content_types = ContentTypeSerializer(many=True, read_only=True) mobile_unit_group = MobileUnitGroupBasicInfoSerializer(many=False, read_only=True) - # geometry = serializers.SerializerMethodField(read_only=True) geometry_coords = serializers.SerializerMethodField(read_only=True) class Meta: From 830af5ab15a6edecfe750a1d6d43a45a384d4c8e Mon Sep 17 00:00:00 2001 From: juuso-j Date: Tue, 7 Mar 2023 10:51:37 +0200 Subject: [PATCH 184/188] Add return statement to get_yit_vehicles function --- street_maintenance/management/commands/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/management/commands/utils.py b/street_maintenance/management/commands/utils.py index 38816da95..a3597e11a 100644 --- a/street_maintenance/management/commands/utils.py +++ b/street_maintenance/management/commands/utils.py @@ -545,7 +545,7 @@ def get_yit_vehicles(access_token): ), " Fetching YIT vehicles {} failed, status code: {}".format( URLS[YIT][VEHICLES], response.status_code ) - response.json() + return response.json() @db.transaction.atomic From 16fbba8550208e8b3b05177c073096cbf0225644 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Mar 2023 08:23:06 +0200 Subject: [PATCH 185/188] Fix argument helt text --- .../management/commands/import_street_maintenance_history.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/street_maintenance/management/commands/import_street_maintenance_history.py b/street_maintenance/management/commands/import_street_maintenance_history.py index cb2d2eb5f..6dcf795d1 100644 --- a/street_maintenance/management/commands/import_street_maintenance_history.py +++ b/street_maintenance/management/commands/import_street_maintenance_history.py @@ -47,7 +47,7 @@ def add_arguments(self, parser): type=str, nargs="+", default=False, - help=("History size in days."), + help=", ".join(PROVIDERS), ) def handle(self, *args, **options): From e6108d7ac68bf41f02583c84dc99fe07c8dffa67 Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Mar 2023 08:24:35 +0200 Subject: [PATCH 186/188] Take multiple arguments --- street_maintenance/tasks.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/street_maintenance/tasks.py b/street_maintenance/tasks.py index 021e1f2af..245186e93 100644 --- a/street_maintenance/tasks.py +++ b/street_maintenance/tasks.py @@ -4,8 +4,8 @@ @shared_task_email -def delete_street_maintenance_history(args, name="delete_street_maintenance_history"): - management.call_command("delete_street_maintenance_history", args) +def delete_street_maintenance_history(*args, name="delete_street_maintenance_history"): + management.call_command("delete_street_maintenance_history", *args) @shared_task_email From 6a38f386444d7e902ad56a4e995245da511816dc Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Mar 2023 08:38:04 +0200 Subject: [PATCH 187/188] Handle multiple providers, fix invalid superclass --- .../delete_street_maintenance_history.py | 78 +++++++++++-------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/street_maintenance/management/commands/delete_street_maintenance_history.py b/street_maintenance/management/commands/delete_street_maintenance_history.py index 1ed0f09cf..ec91b0193 100644 --- a/street_maintenance/management/commands/delete_street_maintenance_history.py +++ b/street_maintenance/management/commands/delete_street_maintenance_history.py @@ -1,8 +1,9 @@ import logging +from django.core.management import BaseCommand + from street_maintenance.models import GeometryHistory, MaintenanceUnit -from .base_import_command import BaseImportCommand from .constants import PROVIDERS logger = logging.getLogger("mobility_data") @@ -11,37 +12,50 @@ PROVIDERS.append("AUTORI") -class Command(BaseImportCommand): +class Command(BaseCommand): def add_arguments(self, parser): - parser.add_argument("provider", nargs="+", help=", ".join(PROVIDERS)) + parser.add_argument( + "providers", + type=str, + nargs="*", + help=", ".join(PROVIDERS), + ) def handle(self, *args, **options): - provider = options["provider"][0].upper() - if provider not in PROVIDERS: - logger.error( - f"Invalid provider argument, choices are: {', '.join(PROVIDERS)}" - ) - return - - logger.info(f"Deleting street maintenance history for {provider}.") - provider = provider.upper() - deleted_units = MaintenanceUnit.objects.filter(provider=provider).delete() - deleted_histories = GeometryHistory.objects.filter(provider=provider).delete() - if "street_maintenance.MaintenanceUnit" in deleted_units[1]: - num_deleted_units = deleted_units[1]["street_maintenance.MaintenanceUnit"] - else: - num_deleted_units = 0 - if "street_maintenance.MaintenanceWork" in deleted_units[1]: - num_deleted_works = deleted_units[1]["street_maintenance.MaintenanceWork"] - else: - num_deleted_works = 0 - if "street_maintenance.GeometryHistory" in deleted_histories[1]: - num_deleted_histories = deleted_histories[1][ - "street_maintenance.GeometryHistory" - ] - else: - num_deleted_histories = 0 - - logger.info(f"GeometryHistorys deleted {num_deleted_histories}.") - logger.info(f"MaintenanceUnits deleted {num_deleted_units}.") - logger.info(f"MaintenanceWorks deleted {num_deleted_works}.") + providers = [p.upper() for p in options.get("providers", None)] + + for provider in providers: + if provider not in PROVIDERS: + logger.error( + f"Invalid providers argument {provider}, choices are: {', '.join(PROVIDERS)}" + ) + continue + + logger.info(f"Deleting street maintenance history for {provider}.") + provider = provider.upper() + deleted_units = MaintenanceUnit.objects.filter(provider=provider).delete() + deleted_histories = GeometryHistory.objects.filter( + provider=provider + ).delete() + if "street_maintenance.MaintenanceUnit" in deleted_units[1]: + num_deleted_units = deleted_units[1][ + "street_maintenance.MaintenanceUnit" + ] + else: + num_deleted_units = 0 + if "street_maintenance.MaintenanceWork" in deleted_units[1]: + num_deleted_works = deleted_units[1][ + "street_maintenance.MaintenanceWork" + ] + else: + num_deleted_works = 0 + if "street_maintenance.GeometryHistory" in deleted_histories[1]: + num_deleted_histories = deleted_histories[1][ + "street_maintenance.GeometryHistory" + ] + else: + num_deleted_histories = 0 + + logger.info(f"GeometryHistorys deleted {num_deleted_histories}.") + logger.info(f"MaintenanceUnits deleted {num_deleted_units}.") + logger.info(f"MaintenanceWorks deleted {num_deleted_works}.") From c9fedc4ecdd5b5ebc5a158fe8d893c723bc10eab Mon Sep 17 00:00:00 2001 From: juuso-j Date: Thu, 9 Mar 2023 11:20:17 +0200 Subject: [PATCH 188/188] Increase max_page_size to 200_000 for Works --- street_maintenance/api/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/street_maintenance/api/views.py b/street_maintenance/api/views.py index ffef8da58..b8440ef1b 100644 --- a/street_maintenance/api/views.py +++ b/street_maintenance/api/views.py @@ -58,7 +58,7 @@ class LargeResultsSetPagination(PageNumberPagination): """ page_size_query_param = "page_size" - max_page_size = 50_000 + max_page_size = 200_000 class ActiveEventsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): @@ -85,6 +85,7 @@ class ActiveEventsViewSet(mixins.ListModelMixin, viewsets.GenericViewSet): ) class MaintenanceWorkViewSet(viewsets.ReadOnlyModelViewSet): serializer_class = MaintenanceWorkSerializer + pagination_class = LargeResultsSetPagination def get_queryset(self): queryset = MaintenanceWork.objects.all()