diff --git a/.env b/.env
index aa949fe6e65b88181d00d68f5aa44e9eec20d4d3..018a40dea282a30930ab94b6d5d12f9f89e661bc 100644
--- a/.env
+++ b/.env
@@ -18,7 +18,7 @@ FROST_POSTGRES_PASSWORD=admin
 
 INITIAL_MEMORY=2G
 MAXIMUM_MEMORY=4G
-GEOSERVER_URL=http://geoserver:8080/geoserver
+GEOSERVER_URL=http://geoserver:8080/geoserver/
 GEOSERVER_USERNAME=admin
 GEOSERVER_PASSWORD=geoserver
 
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 879f650c0bfd56fe66103803cda0efc5040b7d9b..6b6f3ff588370596ec7ce4112c1a9c505b69cbc2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -74,6 +74,9 @@
 #### VueJS 3
 - https://v3.vuejs.org/guide/introduction.html
 
+#### Pinia-Store
+- https://pinia.vuejs.org/getting-started.html
+
 #### Typescript
 - https://www.typescriptlang.org/
 - https://v3.vuejs.org/guide/typescript-support.html
@@ -107,6 +110,19 @@
 
 ### GeoServer
 
+#### Setup
+- https://docs.geoserver.org/main/en/user/production/index.html
+- https://geoserver.geosolutionsgroup.com/edu/en/adv_gsconfig/gsproduction.html
+
+#### WFS
+- https://docs.geoserver.org/main/en/user/services/wfs/basics.html
+- https://docs.geoserver.org/main/en/user/services/wfs/reference.html
+- https://docs.geoserver.org/2.22.x/en/user/extensions/querylayer/index.html
+- https://docs.ogc.org/is/09-025r2/09-025r2.html#50
+- https://docs.geoserver.org/main/en/user/tutorials/cql/cql_tutorial.html#cql-tutorial
+- https://gis.stackexchange.com/questions/132229/cql-filter-that-joins-two-feature-types-in-wfs
+- https://docs.geoserver.org/latest/en/user/filter/function_reference.html#filter-function-reference
+
 - Python library for using the api: https://pypi.org/project/geoserver-restconfig/
 
 ### Django / Backend
diff --git a/backend/data_import/forms/csv_import_job.py b/backend/data_import/forms/csv_import_job.py
index 355858645c2257b76773c291b864762299c24ad9..a3dd46ba12cb839ec585865744084f8665efbb71 100644
--- a/backend/data_import/forms/csv_import_job.py
+++ b/backend/data_import/forms/csv_import_job.py
@@ -9,6 +9,7 @@ class CsvImportJobAdmin(admin.ModelAdmin):
     list_display = (
         's3_file',
         'bucket',
+        'target',
         'is_processed',
         'is_running',
         'is_success',
@@ -19,7 +20,7 @@ class CsvImportJobAdmin(admin.ModelAdmin):
 
     fieldsets = [
         (None, {
-            'fields': (('bucket'), ('s3_file', 'file_size', 'num_rows'), ('time_created', 'is_running', 'is_processed'), ('started_at', 'finished_at', 'execution_time' )),
+            'fields': (('bucket'), ('s3_file', 'file_size', 'num_rows'), 'target', ('time_created', 'is_running', 'is_processed'), ('started_at', 'finished_at', 'execution_time' )),
         }),
         ('Results', {
             'fields': ('is_success', ('data_points_created', 'data_points_failed'), 'validation_error', ),
diff --git a/backend/data_import/forms/csv_parser.py b/backend/data_import/forms/csv_parser.py
index f76e2e888cd4c413f2c75e96852eee285d58398f..5343256b80e535ca2de648c8517d214a8c8f1957 100644
--- a/backend/data_import/forms/csv_parser.py
+++ b/backend/data_import/forms/csv_parser.py
@@ -29,13 +29,18 @@ class CsvParserAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
             'classes': ('tab-basic',),
         }),
         (None, {
-            'fields': ('group', ),
+            'fields': ('location_id_col_num', 'property_id_col_num', 'country_col_num', 'community_col_num' ),
+            'classes': ('tab-optional',),
+        }),
+        (None, {
+            'fields': ('target', 'table_prefix', 'group', ),
             'classes': ('tab-group',),
         }),
     ]
 
     tabs = [
         ("Basic Information", ["tab-basic"]),
+        ("Optional Information", ["tab-optional"]),
         ("Extra-Columns", ["tab-extra-columns-inline"]),
         ("Include-Criteria", ["tab-include-criteria-inline"]),
         ("Group", ["tab-group"]),
diff --git a/backend/data_import/lib/PostgisImporter.py b/backend/data_import/lib/PostgisImporter.py
new file mode 100644
index 0000000000000000000000000000000000000000..3aea6723a6c8157bc0d62b879367f1ff1e1401a4
--- /dev/null
+++ b/backend/data_import/lib/PostgisImporter.py
@@ -0,0 +1,577 @@
+import csv
+
+from django.db import connections   # noqa
+
+from data_import.models import CsvParser, CsvImportJob, CsvParserExtraColumn, CsvIncludeCriteria  # noqa
+from data_import.models import WfsImportRecord, WFS, STA, STA_THING, STA_PROPERTY, STA_OBSERVATION # noqa
+from data_import.lib.utils import is_row_included   # noqa
+
+class PostgisImporter:
+
+    def __init__(self, job: CsvImportJob):
+
+        self.job = job
+        self.parser = job.bucket.csv_parser
+
+        table_prefix = self.parser.table_prefix
+
+        self.feature_table = "{}_feature".format(table_prefix)
+        self.property_table = "{}_property".format(table_prefix)
+        self.timeseries_table = "{}_timeseries".format(table_prefix)
+        self.timerecord_table = "{}_timerecord".format(table_prefix)
+
+        self.error = False
+        self.logs = []
+
+
+    def import_csv_to_wfs(self):
+
+        self.create_tables_if_not_exist()
+
+        file_name = self.job.s3_file.split('/')[-1]
+
+        with open(file_name, newline='') as csvfile:
+            reader = csv.reader(csvfile, delimiter=',')
+            next(reader)  # skip header
+
+            batch_size = 1000
+            batch = []
+            count = 0
+
+            p = self.parser
+            extra_columns = CsvParserExtraColumn.objects.filter(parser=p)
+            include_criteria = CsvIncludeCriteria.objects.filter(parser=p)
+
+            for row in reader:
+
+                if len(row) == 0:
+                    continue
+                if is_row_included(row, include_criteria):
+
+                    location_props = {}
+                    property_props = {}
+                    record_props = {}
+
+                    for ec in extra_columns:
+                        if ec.related_entity == STA_THING:
+                            location_props[ec.col_name] = row[ec.col_num]
+                        if ec.related_entity == STA_PROPERTY:
+                            property_props[ec.col_name] = row[ec.col_num]
+                        if ec.related_entity == STA_OBSERVATION:
+                            record_props[ec.col_name] = row[ec.col_num]
+
+                    if row[p.time_col_num] == 'NA':
+                        continue
+
+                    # if is point data
+                    # TODO: adapt also for polygon data
+
+                    batch.append(
+                        WfsImportRecord(
+                            location_name=row[p.station_col_num],
+                            lat=row[p.lat_col_num],
+                            lon=row[p.lon_col_num],
+                            value=row[p.value_col_num],
+                            date=row[p.time_col_num],
+                            unit=row[p.unit_col_num],
+                            property_name=row[p.property_col_num],
+                            bucket_name=self.job.bucket.name,
+                            import_job=self.job.id,
+                            location_id=row[p.location_id_col_num] if p.location_id_col_num else None,
+                            property_id=row[p.property_id_col_num] if p.property_id_col_num else None,
+                            country=row[p.country_col_num] if p.country_col_num else None,
+                            community=row[p.community_col_num] if p.community_col_num else None,
+                            location_properties=location_props,
+                            property_properties=property_props,
+                            record_properties=record_props
+                        )
+                    )
+
+                    count += 1
+                    if count % batch_size == 0:
+                        WfsImportRecord.objects.using('geoserver').bulk_create(batch)
+                        batch = []
+                        print('{} records created.'.format(count))
+
+
+    def write_data(self):
+
+        c = connections["geoserver"].cursor()
+
+        self.write_features(c)
+        self.write_properties(c)
+        self.write_timeseries(c)
+        self.write_timerecords(c)
+
+        self.update_properties_bbox(c)
+        self.update_timeseries_values(c)
+        self.update_features(c)
+
+        try:
+            c.execute("""DELETE FROM geoserver.data_import_wfsimportrecord""")
+            print('Import done.')
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+        finally:
+            c.close()
+
+
+    def write_features(self, c):
+
+        try:
+            c.execute("""
+                insert into geoserver.{table} (name, lat, lon, geom, gtype, srid, country, import_id, created_at{custom_columns})
+                    select 
+                        d.location_name,
+                        cast(d.lat as double precision),
+                        cast(d.lon as double precision),
+                        st_point(cast(d.lon as double precision), cast(d.lat as double precision), 4326),
+                        'Point',
+                        '4326',
+                        max(d.country),
+                        max(d.import_job),
+                        now()
+                        {custom_column_selects}
+                    from geoserver.data_import_wfsimportrecord d
+                    group by (d.location_name, d.lat, d.lon, d.location_properties)
+                    on conflict do nothing;
+            """.format(
+                    table=self.feature_table,
+                    custom_columns=self.get_custom_columns(STA_THING),
+                    custom_column_selects=self.get_custom_column_selects(STA_THING, 'd.location_properties')
+                ))
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+        print('Features inserted.')
+
+
+    def write_properties(self, c):
+
+        try:
+            c.execute("""
+                insert into geoserver.{table} (name, technical_id, min_lat, min_lon, max_lat, max_lon{custom_columns})
+                    select 
+                        d.property_name, 
+                        d.property_id, 
+                        MIN(cast(d.lat as double precision)), 
+                        MIN(cast(d.lon as double precision)), 
+                        MAX(cast(d.lat as double precision)), 
+                        MAX(cast(d.lon as double precision))
+                        {custom_column_selects}
+                    from geoserver.data_import_wfsimportrecord d
+                    group by (d.property_name, d.property_id, d.property_properties)
+                    on conflict do nothing;
+            """.format(
+                    table=self.property_table,
+                    custom_columns=self.get_custom_columns(STA_PROPERTY),
+                    custom_column_selects=self.get_custom_column_selects(STA_PROPERTY, 'd.property_properties')
+                )
+            )
+
+            print('Properties inserted.')
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+    def write_timeseries(self, c):
+
+        # if point => take over Point-Geometry
+        # if polygon => create BBox
+        try:
+            c.execute("""
+            insert into geoserver.{timeseries_table} (feature_id, property_id, unit, min_value, max_value, min_date, max_date, lat, lon, geom, gtype, srid)
+                select
+                    (select f.id from {feature_table} f where f.name = d.location_name and f.lat = cast(d.lat as double precision) and f.lon = cast(d.lon as double precision)),
+                    (select p.id from {property_table} p where p.name = d.property_name),
+                    d.unit,
+                    MIN(cast(d.value as double precision)),
+                    MAX(cast(d.value as double precision)),
+                    MIN(to_timestamp(d.date, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC'),
+                    MAX(to_timestamp(d.date, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC'),
+                    cast(d.lat as double precision),
+                    cast(d.lon as double precision),
+                    st_point(cast(d.lon as double precision), cast(d.lat as double precision), 4326),
+                    'Point',
+                    '4326'
+                    from geoserver.data_import_wfsimportrecord d
+                    group by (
+                        d.location_name,
+                        d.lat,
+                        d.lon,
+                        d.property_name,
+                        d.unit
+                    )
+                on conflict do nothing;
+            """.format(timeseries_table=self.timeseries_table, feature_table=self.feature_table, property_table=self.property_table))
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+        print('TimeSeries inserted.')
+
+
+    def write_timerecords(self, c):
+
+        try:
+            c.execute("""
+                insert into geoserver.{timerecord_table} (timeseries_id, date, value_text, value_number, lat, lon, geom, gtype, srid, import_id{custom_columns})
+                    select
+                        (select id from {timeseries_table} t where t.feature_id = cf.id and t.property_id = cp.id),
+                        to_timestamp(d.date, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC',
+                        d.value,
+                        cast(d.value as double precision),
+                        cast(d.lat as double precision),
+                        cast(d.lon as double precision),
+                        st_point(cast(d.lon as double precision), cast(d.lat as double precision), 4326),
+                        'Point',
+                        '4326',
+                        d.import_job
+                        {custom_column_selects}
+                        from geoserver.data_import_wfsimportrecord d
+                        inner join geoserver.{feature_table} cf on d.location_name = cf.name and cast(d.lat as double precision) = cf.lat and cast(d.lon as double precision) = cf.lon
+                        inner join geoserver.{property_table} cp on d.property_name = cp.name;
+            """.format(
+                timerecord_table=self.timerecord_table,
+                feature_table=self.feature_table,
+                property_table=self.property_table,
+                timeseries_table=self.timeseries_table,
+                custom_columns=self.get_custom_columns(STA_OBSERVATION),
+                custom_column_selects=self.get_custom_column_selects(STA_OBSERVATION, 'd.record_properties')
+            ))
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+        print('TimeRecords inserted.')
+
+
+    def update_features(self, c):
+
+        stmt_params = [
+            ('min_timerecord', 'min_date', 'MIN'),
+            ('max_timerecord', 'max_date', 'MAX'),
+        ]
+
+        try:
+            for params in stmt_params:
+                c.execute("""
+                    update geoserver.{feature_table} cf
+                        set {col1} = x.aggr_date
+                        from (
+                            select
+                                {aggr_func}(ts.{col2}) as aggr_date,
+                                ts.feature_id
+                            from {timeseries_table} ts
+                            group by ts.feature_id
+                        ) as x
+                        where cf.id = x.feature_id
+                    """.format(
+                        feature_table=self.feature_table,
+                        timeseries_table=self.timeseries_table,
+                        col1=params[0],
+                        col2=params[1],
+                        aggr_func=params[2])
+                )
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+        print('Min- and Max-Timerecords of Features updated.')
+
+    def update_properties_bbox(self, c):
+
+        stmt_params = [
+            ('min_lat', 'lat', 'MIN', '<'),
+            ('min_lon', 'lon', 'MIN', '<'),
+            ('max_lat', 'lat', 'MAX', '>'),
+            ('max_lon', 'lon', 'MAX', '>'),
+        ]
+
+        try:
+            for params in stmt_params:
+                c.execute("""
+                    update geoserver.{table_name} cp
+                        set {col1} = x.aggr_value
+                        from (
+                            select {aggr_func}(cast(d.{col2} as double precision)) as aggr_value,
+                            property_name
+                            from geoserver.data_import_wfsimportrecord d
+                            group by (d.property_name)
+                        ) as x
+                        where cp.name = x.property_name
+                        and x.aggr_value {operator} cp.{col1}
+                """.format(
+                    table_name=self.property_table,
+                    col1=params[0],
+                    col2=params[1],
+                    aggr_func=params[2],
+                    operator=params[3])
+                )
+
+            c.execute("""
+                update geoserver.{} cp
+                    set geom = ST_MakeEnvelope(cp.min_lon, cp.min_lat, cp.max_lon, cp.max_lat),
+                    gtype = 'Point',
+                    srid = '4326'
+            """.format(self.property_table))
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+        print('Bounding-Box of Properties updated.')
+
+    def update_timeseries_values(self, c):
+
+        stmt_params = [
+            ('min_value', 'cast(d.value as double precision)', 'MIN', '<'),
+            ('max_value', 'cast(d.value as double precision)', 'MAX', '>'),
+            ('min_date', "to_timestamp(d.date, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC'", 'MIN', '<'),
+            ('max_date', "to_timestamp(d.date, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC'", 'MAX', '>'),
+        ]
+
+        try:
+            for params in stmt_params:
+                c.execute("""
+                    update geoserver.{timeseries_table} ts
+                        set {col1} = x.updated_value
+                        from (
+                            select {aggr_func}({col2}) as updated_value,
+                            d.property_name,
+                            d.location_name,
+                            cast(d.lat as double precision),
+                            cast(d.lon as double precision),
+                            d.unit
+                            from geoserver.data_import_wfsimportrecord d
+                            group by (
+                                d.property_name,
+                                d.location_name,
+                                d.lat,
+                                d.lon,
+                                d.unit
+                            )
+                        ) as x
+                        where ts.property_id = (select p.id from geoserver.{property_table} p where x.property_name = p.name)
+                        and ts.feature_id = (select f.id from geoserver.{feature_table} f where x.location_name = f.name and x.lat = f.lat and x.lon = f.lon)
+                        and ts.unit = x.unit
+                        and x.updated_value {operator} ts.{col1};
+                """.format(
+                    timeseries_table=self.timeseries_table,
+                    feature_table=self.feature_table,
+                    property_table=self.property_table,
+                    col1=params[0],
+                    col2=params[1],
+                    aggr_func=params[2],
+                    operator=params[3])
+                )
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+
+        print('TimeSeries updated.')
+
+
+    def create_tables_if_not_exist(self):
+        c = connections["geoserver"].cursor()
+
+        stmts = []
+        stmts.append("""
+            create table if not exists geoserver.data_import_wfsimportrecord
+            (
+                id                  bigint generated by default as identity primary key,
+                location_id         varchar(1000),
+                location_name       varchar(1000),
+                type_name           varchar(1000),
+                period_start        varchar(1000),
+                period_end          varchar(1000),
+                lat                 varchar(1000),
+                lon                 varchar(1000),
+                srid                varchar(1000),
+                geom                varchar(4000),
+                gtype               varchar(1000),
+                country             varchar(1000),
+                community           varchar(1000),
+                corine              varchar(1000),
+                value               varchar(1000),
+                date                varchar(1000),
+                unit                varchar(1000),
+                property_name       varchar(1000),
+                property_id         varchar(1000),
+                location_properties jsonb,
+                property_properties jsonb,
+                record_properties   jsonb,
+                import_job          integer                  not null
+                    constraint data_import_wfsimportrecord_import_job_check
+                        check (import_job >= 0),
+                created_at          timestamp with time zone not null,
+                bucket_name         varchar                  not null
+            );""")
+
+        stmts.append("""
+            create table if not exists geoserver.{feature_table}
+            (
+                id         integer generated always as identity
+                    constraint {feature_table}_pk
+                        primary key,
+                name       varchar not null,
+                lat        double precision,
+                lon        double precision,
+                geom       geometry,
+                gtype      varchar,
+                srid       varchar,
+                country    varchar,
+                community  varchar,
+                import_id  integer not null,
+                min_timerecord timestamp,
+                max_timerecord timestamp,
+                created_at timestamp
+                {properties}
+            );""".format(feature_table=self.feature_table, properties=self.get_custom_column_defs(STA_THING)))
+
+        stmts.append("create unique index if not exists unique_{table} on geoserver.{table} (name, lat, lon);".format(table=self.feature_table))
+
+        stmts.append(
+           """ 
+            create table if not exists geoserver.{property_table}
+            (
+                id           integer generated always as identity
+                    constraint {property_table}_pk
+                        primary key,
+                name         varchar not null,
+                technical_id varchar,
+                min_lat      numeric,
+                min_lon      numeric,
+                max_lat      numeric,
+                max_lon      numeric,
+                geom         geometry,
+                gtype        varchar,
+                srid         varchar
+                {properties}
+            );""".format(property_table=self.property_table, properties=self.get_custom_column_defs(STA_PROPERTY)))
+
+        stmts.append("create unique index if not exists unique_{table} on geoserver.{table} (name);".format(table=self.property_table))
+
+        stmts.append("""
+            create table if not exists geoserver.{timeseries_table}
+            (
+                id          integer generated always as identity
+                    constraint {timeseries_table}_pk
+                        primary key,
+                feature_id  integer not null
+                    constraint {timeseries_table}_{feature_table}_id_fk
+                        references geoserver.{feature_table},
+                property_id integer not null
+                    constraint {timeseries_table}_{property_table}_id_fk
+                        references geoserver.{property_table},
+                unit        varchar,
+                lat         numeric,
+                lon         numeric,
+                geom        geometry,
+                gtype       varchar,
+                srid        varchar,
+                min_value   double precision,
+                max_value   double precision,
+                min_date    timestamp,
+                max_date    timestamp
+            );""".format(timeseries_table=self.timeseries_table, feature_table=self.feature_table, property_table=self.property_table))
+
+        stmts.append("create unique index if not exists unique_{table} on geoserver.{table} (feature_id, property_id, unit);".format(table=self.timeseries_table))
+
+        stmts.append("create index if not exists {table}_timeseries_id_index on geoserver.{table} (feature_id);".format(table=self.timeseries_table))
+
+        stmts.append("""
+            create table if not exists geoserver.{timerecord_table}
+            (
+                id            integer generated always as identity
+                    constraint {timerecord_table}_pk
+                        primary key,
+                timeseries_id integer   not null
+                    constraint "{timerecord_table}_{timeseries_table}_id_fk"
+                        references geoserver.{timeseries_table},
+                date          timestamp not null,
+                value_text    varchar,
+                lat           numeric,
+                lon           numeric,
+                geom          geometry,
+                gtype         varchar,
+                import_id     integer   not null,
+                value_number  double precision,
+                srid          varchar
+                {properties}
+            );
+        """.format(
+                timerecord_table=self.timerecord_table,
+                timeseries_table=self.timeseries_table,
+                properties=self.get_custom_column_defs(STA_OBSERVATION)
+            )
+        )
+
+        stmts.append("create index if not exists {table}_timeseries_id_index on geoserver.{table} (timeseries_id);".format(table=self.timerecord_table))
+
+        tables = [
+            "data_import_wfsimportrecord",
+            self.feature_table,
+            self.property_table,
+            self.timeseries_table,
+            self.timerecord_table
+        ]
+
+        try:
+            for stmt in stmts:
+                c.execute(stmt)
+
+            for table in tables:
+                c.execute("alter table geoserver.{table} owner to admin;".format(table=table))
+
+        except Exception as e:
+            self.logs.append(str(e))
+            self.error = True
+        finally:
+            c.close()
+
+    def get_custom_columns(self, entity_type: str):
+        result = ""
+        for extra_column in CsvParserExtraColumn.objects.filter(parser=self.parser).order_by("id"):
+            if extra_column.related_entity == entity_type:
+                result += ", " + extra_column.col_name
+        return result
+
+    def get_custom_column_selects(self, entity_type: str, field: str):
+        columns = []
+        for extra_column in CsvParserExtraColumn.objects.filter(parser=self.parser):
+
+            if extra_column.related_entity == entity_type:
+                value = field + "->>'" + extra_column.col_name + "'"
+
+                if extra_column.type_in_db == 'timestamp':
+                    columns.append("to_timestamp({}, 'YYYY-MM-DDThh24:mi:ss')::timestamp without time zone at time zone 'Etc/UTC'".format(value))
+                elif extra_column.type_in_db == 'double precision':
+                    columns.append("(SELECT CASE WHEN {value}~E'[0-9]+\.?[0-9]*' THEN CAST({value} as double precision) ELSE NULL END)".format(value=value))
+                elif extra_column.type_in_db == 'integer':
+                    columns.append("(SELECT CASE WHEN {value}~E'[0-9]+' THEN CAST({value} as integer) ELSE NULL END)".format(value=value))
+                else:
+                    columns.append(value)
+
+        result = ""
+        if len(columns) > 0:
+            result = ",\n" + ",\n".join(columns)
+        return result
+
+    def get_custom_column_defs(self, entity_type: str):
+        columns = []
+        for extra_column in CsvParserExtraColumn.objects.filter(parser=self.parser):
+            if extra_column.related_entity == entity_type:
+                columns.append(extra_column.col_name + " " + extra_column.type_in_db)
+
+        result = ""
+        if len(columns) > 0:
+            result = ",\n" + ",\n".join(columns)
+        return result
\ No newline at end of file
diff --git a/backend/data_import/api/StaApi.py b/backend/data_import/lib/StaImporter.py
similarity index 71%
rename from backend/data_import/api/StaApi.py
rename to backend/data_import/lib/StaImporter.py
index f3c7766bac24ea244c336e8b487837c115e2ff0f..f1ed4d8c8f7dd404f8e94d27995b26bcfcd669eb 100644
--- a/backend/data_import/api/StaApi.py
+++ b/backend/data_import/lib/StaImporter.py
@@ -1,9 +1,14 @@
 import requests
+import csv
 
 from data_import.models import CsvImportJob, ExtendedPointData, PointData  # noqa
+from data_import.models import CsvParser, CsvImportJob, CsvParserExtraColumn, CsvIncludeCriteria, PointData, ExtendedPointData # noqa
+from main.models import StaThingProperty    # noqa
+from data_import.models import STA_THING # noqa
+from data_import.lib.utils import is_row_included   # noqa
 
 
-class StaApi:
+class StaImporter:
 
     def __init__(self, job: CsvImportJob):
         self.error = False
@@ -14,6 +19,42 @@ class StaApi:
         self.sta_url = self.sta_endpoint.base_url.rstrip('/') + '/v1.1/'
         self.auth = (self.sta_endpoint.username, self.sta_endpoint.password)
 
+
+    def import_csv_in_sta(self):
+        file_name = self.import_job.s3_file.split('/')[-1]
+
+        with open(file_name, newline='') as csvfile:
+
+            reader = csv.reader(csvfile, delimiter=',')
+            next(reader)  # skip header
+            thing_props = StaThingProperty.objects.filter(endpoint=self.import_job.bucket.sta_endpoint)
+            extra_columns = CsvParserExtraColumn.objects.filter(parser=self.import_job.bucket.csv_parser)
+
+            include_criteria = CsvIncludeCriteria.objects.filter(parser=self.import_job.bucket.csv_parser)
+
+            rows_succeeded = 0
+            rows_failed = 0
+            for row in reader:
+
+                if is_row_included(row, include_criteria):
+
+                    point_data = create_point_data(self.import_job, row)
+                    extended_data = create_extended_data(point_data, extra_columns, thing_props, row)
+
+                    self.import_point_data(point_data, extended_data)
+
+                    if self.error is True:
+                        point_data.validation_error = "\n".join(self.logs)
+                        point_data.save()
+                        for data in extended_data:
+                            data.save()
+                        rows_failed += 1
+                    else:
+                        rows_succeeded += 1
+                    self.error = False
+                    self.logs = []
+
+
     def import_point_data(self, point_data: PointData, extended_point_data: list[ExtendedPointData]):
 
         location = self.get_location_json(point_data)
@@ -125,7 +166,6 @@ class StaApi:
         print('error {}: {}'.format(route, content))
         return False
 
-
     def get_thing_json(self, point_data: PointData, extended_data: list[ExtendedPointData]):
         thing = {
             "name": point_data.thing_name,
@@ -213,3 +253,46 @@ def sanitize_str(text: str):
     result = text.replace("'", "''")
     result = result.replace("+", "%2b")
     return result.replace("/", "%2F")
+
+
+def create_point_data(job: CsvImportJob, row):
+    p: CsvParser = job.bucket.csv_parser
+
+    point_data = PointData(
+        import_job = job,
+        thing_name = row[p.station_col_num],
+        location_name = row[p.station_col_num],
+        coord_lat = row[p.lat_col_num],
+        coord_lon = row[p.lon_col_num],
+        # geometry = '',
+        property = row[p.property_col_num],
+        # sensor = '',
+        result_value = row[p.value_col_num],
+        result_unit = row[p.unit_col_num],
+        result_time = row[p.time_col_num]
+    )
+    return point_data
+
+
+def create_extended_data(point_data: PointData, extra_columns: list[CsvParserExtraColumn], thing_props: list[StaThingProperty], row):
+    result = []
+
+    for prop in thing_props:
+        extended_data = ExtendedPointData(
+            point_data = point_data,
+            related_entity = STA_THING,
+            name = prop.property_key,
+            value = prop.property_value
+        )
+        result.append(extended_data)
+
+    for column in extra_columns:
+        extended_data = ExtendedPointData(
+            point_data = point_data,
+            related_entity = column.related_entity,
+            name = column.col_name,
+            value = row[column.col_num]
+        )
+        result.append(extended_data)
+
+    return result
diff --git a/backend/data_import/lib/utils.py b/backend/data_import/lib/utils.py
new file mode 100644
index 0000000000000000000000000000000000000000..8b4e87df28591ac80dec43da7788ec1e98dc554c
--- /dev/null
+++ b/backend/data_import/lib/utils.py
@@ -0,0 +1,12 @@
+
+from data_import.models import CsvIncludeCriteria # noqa
+
+
+def is_row_included(row, include_criteria: list[CsvIncludeCriteria]) -> bool:
+    if len(include_criteria) > 0:
+        for criteria in include_criteria:
+            if row[criteria.col_num] == criteria.text_value:
+                return True
+        return False
+    else:
+        return True
diff --git a/backend/data_import/migrations/0004_wfsimportrecord_csvimportjob_target_and_more.py b/backend/data_import/migrations/0004_wfsimportrecord_csvimportjob_target_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..acc57479a857ee447fd3691cd481f4264fcf70ad
--- /dev/null
+++ b/backend/data_import/migrations/0004_wfsimportrecord_csvimportjob_target_and_more.py
@@ -0,0 +1,74 @@
+# Generated by Django 4.2.16 on 2024-11-15 10:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('data_import', '0003_csvimportjob_finished_at_csvimportjob_started_at_and_more'),
+    ]
+
+    operations = [
+        migrations.CreateModel(
+            name='WfsImportRecord',
+            fields=[
+                ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
+                ('location_id', models.CharField(blank=True, max_length=1000, null=True)),
+                ('location_name', models.CharField(blank=True, max_length=1000, null=True)),
+                ('type_id', models.CharField(blank=True, max_length=1000, null=True)),
+                ('type_name', models.CharField(blank=True, max_length=1000, null=True)),
+                ('period_start', models.CharField(blank=True, max_length=1000, null=True)),
+                ('period_end', models.CharField(blank=True, max_length=1000, null=True)),
+                ('lat', models.CharField(blank=True, max_length=1000, null=True)),
+                ('lon', models.CharField(blank=True, max_length=1000, null=True)),
+                ('srid', models.CharField(blank=True, max_length=1000, null=True)),
+                ('geom', models.CharField(blank=True, max_length=4000, null=True)),
+                ('gtype', models.CharField(blank=True, max_length=1000, null=True)),
+                ('country', models.CharField(blank=True, max_length=1000, null=True)),
+                ('community', models.CharField(blank=True, max_length=1000, null=True)),
+                ('corine', models.CharField(blank=True, max_length=1000, null=True)),
+                ('value', models.CharField(blank=True, max_length=1000, null=True)),
+                ('date', models.CharField(blank=True, max_length=1000, null=True)),
+                ('unit', models.CharField(blank=True, max_length=1000, null=True)),
+                ('property_name', models.CharField(blank=True, max_length=1000, null=True)),
+                ('property_id', models.CharField(blank=True, max_length=1000, null=True)),
+                ('location_properties', models.JSONField(blank=True, null=True)),
+                ('property_properties', models.JSONField(blank=True, null=True)),
+                ('record_properties', models.JSONField(blank=True, null=True)),
+                ('bucket_name', models.CharField(max_length=1000)),
+                ('import_job', models.PositiveIntegerField()),
+                ('created_at', models.DateTimeField(auto_now_add=True)),
+            ],
+        ),
+        migrations.AddField(
+            model_name='csvimportjob',
+            name='target',
+            field=models.CharField(blank=True, max_length=1000, null=True),
+        ),
+        migrations.AddField(
+            model_name='csvparser',
+            name='community_col_num',
+            field=models.IntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='csvparser',
+            name='country_col_num',
+            field=models.IntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='csvparser',
+            name='location_id_col_num',
+            field=models.IntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='csvparser',
+            name='property_id_col_num',
+            field=models.IntegerField(blank=True, null=True),
+        ),
+        migrations.AddField(
+            model_name='csvparser',
+            name='target',
+            field=models.CharField(choices=[('WFS', 'Web Feature Service'), ('STA', 'Sensorthings API')], default='WFS', max_length=20),
+        ),
+    ]
diff --git a/backend/data_import/migrations/0005_csvparserextracolumn_type_in_db.py b/backend/data_import/migrations/0005_csvparserextracolumn_type_in_db.py
new file mode 100644
index 0000000000000000000000000000000000000000..da7b2140be019a7906e8846bec2e54538155cf51
--- /dev/null
+++ b/backend/data_import/migrations/0005_csvparserextracolumn_type_in_db.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.16 on 2024-12-13 13:39
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('data_import', '0004_wfsimportrecord_csvimportjob_target_and_more'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='csvparserextracolumn',
+            name='type_in_db',
+            field=models.CharField(choices=[('varchar', 'string'), ('integer', 'integer'), ('double precision', 'float'), ('timestamp', 'date')], default='varchar', max_length=20),
+        ),
+    ]
diff --git a/backend/data_import/migrations/0006_csvparser_table_prefix.py b/backend/data_import/migrations/0006_csvparser_table_prefix.py
new file mode 100644
index 0000000000000000000000000000000000000000..01ba855ff23c320e9b83ce5f8dfba3b7f11ed2a9
--- /dev/null
+++ b/backend/data_import/migrations/0006_csvparser_table_prefix.py
@@ -0,0 +1,19 @@
+# Generated by Django 4.2.16 on 2024-12-13 16:21
+
+import data_import.models
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('data_import', '0005_csvparserextracolumn_type_in_db'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='csvparser',
+            name='table_prefix',
+            field=models.CharField(blank=True, max_length=20, null=True, unique=True, validators=[data_import.models.validate_prefix]),
+        ),
+    ]
diff --git a/backend/data_import/migrations/0007_remove_wfsimportrecord_type_id.py b/backend/data_import/migrations/0007_remove_wfsimportrecord_type_id.py
new file mode 100644
index 0000000000000000000000000000000000000000..5595a530daaa0db5559a002c753e89cc553b4be1
--- /dev/null
+++ b/backend/data_import/migrations/0007_remove_wfsimportrecord_type_id.py
@@ -0,0 +1,17 @@
+# Generated by Django 4.2.16 on 2024-12-13 16:30
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('data_import', '0006_csvparser_table_prefix'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='wfsimportrecord',
+            name='type_id',
+        ),
+    ]
diff --git a/backend/data_import/models.py b/backend/data_import/models.py
index 3f91c1950ae548c2bf789ebc8cce7c74055c1b31..fcc75fcea8b80d1831bace57d9f4dcc217dadd10 100644
--- a/backend/data_import/models.py
+++ b/backend/data_import/models.py
@@ -1,15 +1,41 @@
 from django.db import models # noqa
 from django.contrib.auth.models import Group    # noqa
+import re
+from django.core.exceptions import ValidationError    # noqa
 
 STA_THING = 'thing'
+STA_PROPERTY = 'property'
+STA_OBSERVATION = 'observation'
 
-STA_COLUMN_TYPES = (
+CSV_ENTITIES = (
     (STA_THING, 'thing'),
-    ('property', 'property'),
-    ('observation', 'observation'),
+    (STA_PROPERTY, 'property'),
+    (STA_OBSERVATION, 'observation'),
+)
+
+DB_COLUMN_TYPES = (
+    ('varchar', 'string'),
+    ('integer', 'integer'),
+    ('double precision', 'float'),
+    ('timestamp', 'date'),
+)
+
+WFS = 'WFS'
+STA = 'STA'
+
+TARGETS = (
+    (WFS, 'Web Feature Service'),
+    (STA, 'Sensorthings API'),
 )
 
 
+def validate_prefix(value):
+    if len(value) < 3 or len(value) > 20:
+        raise ValidationError("Length must be between 3 and 20 characters.")
+    if not re.match(r'^[a-z]*$', value):
+        raise ValidationError('Only lowercase-letters are allowed.')
+
+
 class CsvParser(models.Model):
     lat_col_num = models.IntegerField()
     lon_col_num = models.IntegerField()
@@ -18,6 +44,12 @@ class CsvParser(models.Model):
     value_col_num = models.IntegerField()
     unit_col_num = models.IntegerField()
     time_col_num = models.IntegerField()
+    location_id_col_num = models.IntegerField(blank=True, null=True)
+    property_id_col_num = models.IntegerField(blank=True, null=True)
+    country_col_num = models.IntegerField(blank=True, null=True)
+    community_col_num = models.IntegerField(blank=True, null=True)
+    table_prefix = models.CharField(max_length=20, blank=True, null=True, unique=True, validators=[validate_prefix])
+    target = models.CharField(max_length=20, choices=TARGETS, default=WFS)
     group = models.ForeignKey(Group, on_delete=models.CASCADE, null=True)
 
     class Meta:
@@ -25,13 +57,14 @@ class CsvParser(models.Model):
         verbose_name_plural = "CSV-Parser"
 
     def __str__(self):
-        return 'Parser ' + str(self.id)
+        return '{} Parser ({}) {}'.format(self.target, str(self.group), str(self.id))
 
 class CsvParserExtraColumn(models.Model):
     parser = models.ForeignKey(CsvParser, on_delete=models.CASCADE)
-    related_entity = models.CharField(max_length=20, choices=STA_COLUMN_TYPES, default=STA_THING)
+    related_entity = models.CharField(max_length=20, choices=CSV_ENTITIES, default=STA_THING)
     col_num = models.IntegerField()
     col_name = models.CharField(max_length=100)
+    type_in_db = models.CharField(max_length=20, choices=DB_COLUMN_TYPES, default='varchar')
 
 class CsvIncludeCriteria(models.Model):
     parser = models.ForeignKey(CsvParser, on_delete=models.CASCADE)
@@ -54,6 +87,7 @@ class CsvImportJob(models.Model):
     created_at = models.DateTimeField(auto_now_add=True)
     started_at = models.DateTimeField(blank=True, null=True)
     finished_at = models.DateTimeField(blank=True, null=True)
+    target = models.CharField(max_length=1000, blank=True, null=True)
 
     class Meta:
         verbose_name = "CSV-Import-Job"
@@ -81,10 +115,37 @@ class PointData(models.Model):
 
 class ExtendedPointData(models.Model):
     point_data = models.ForeignKey(PointData, on_delete=models.CASCADE)
-    related_entity = models.CharField(max_length=20, choices=STA_COLUMN_TYPES, default='thing')
+    related_entity = models.CharField(max_length=20, choices=CSV_ENTITIES, default='thing')
     name = models.CharField(max_length=100)
     value = models.CharField(max_length=1000)
 
     class Meta:
         verbose_name = "Additional Value"
         verbose_name_plural = "Additional Values"
+
+
+class WfsImportRecord(models.Model):
+    location_id = models.CharField(max_length=1000, blank=True, null=True)
+    location_name = models.CharField(max_length=1000, blank=True, null=True)
+    type_name = models.CharField(max_length=1000, blank=True, null=True)
+    period_start = models.CharField(max_length=1000, blank=True, null=True)
+    period_end = models.CharField(max_length=1000, blank=True, null=True)
+    lat = models.CharField(max_length=1000, blank=True, null=True)
+    lon = models.CharField(max_length=1000, blank=True, null=True)
+    srid = models.CharField(max_length=1000, blank=True, null=True)
+    geom = models.CharField(max_length=4000, blank=True, null=True)
+    gtype = models.CharField(max_length=1000, blank=True, null=True)
+    country = models.CharField(max_length=1000, blank=True, null=True)
+    community = models.CharField(max_length=1000, blank=True, null=True)
+    corine = models.CharField(max_length=1000, blank=True, null=True)
+    value = models.CharField(max_length=1000, blank=True, null=True)
+    date = models.CharField(max_length=1000, blank=True, null=True)
+    unit = models.CharField(max_length=1000, blank=True, null=True)
+    property_name = models.CharField(max_length=1000, blank=True, null=True)
+    property_id = models.CharField(max_length=1000, blank=True, null=True)
+    location_properties = models.JSONField(blank=True, null=True)
+    property_properties = models.JSONField(blank=True, null=True)
+    record_properties = models.JSONField(blank=True, null=True)
+    bucket_name = models.CharField(max_length=1000)
+    import_job = models.PositiveIntegerField()
+    created_at = models.DateTimeField(auto_now_add=True)
\ No newline at end of file
diff --git a/backend/gdi-admin/settings.py b/backend/gdi-admin/settings.py
index ebce23e05d9a88f0dde08b34fd2a993f7a58f5cd..a6c7f39d8aeb1ef79a75641ee8297e00868c3cf4 100644
--- a/backend/gdi-admin/settings.py
+++ b/backend/gdi-admin/settings.py
@@ -100,7 +100,19 @@ DATABASES = {
         'HOST': os.environ.get('POSTGRES_HOST'),
         'PORT': 5432,
         'OPTIONS': {'sslmode': os.environ.get('POSTGRES_SSLMODE')},
-    }
+    },
+    "geoserver": {
+        'ENGINE': 'django.db.backends.postgresql',
+        "NAME": os.environ.get('POSTGRES_DB'),
+        "USER": "admin",
+        "PASSWORD": "admin",
+        'HOST': os.environ.get('POSTGRES_HOST'),
+        'PORT': 5432,
+        'OPTIONS': {
+            'sslmode': os.environ.get('POSTGRES_SSLMODE'),
+            'options': '-c search_path=geoserver,postgis'
+        },
+    },
 }
 
 
diff --git a/backend/gdi-admin/urls.py b/backend/gdi-admin/urls.py
index 897b54eb6066b4309f2b0d8df53b4882165af295..c2018726deac69c9b70c871348456d7049c5fbdc 100644
--- a/backend/gdi-admin/urls.py
+++ b/backend/gdi-admin/urls.py
@@ -16,7 +16,7 @@ Including another URLconf
 from django.contrib import admin
 from django.urls import include, path, re_path
 
-from main.views import views, aggregation_requests, download_requests, timeseries_request, sta_requests
+from main.views import views, aggregation_requests, download_requests, timeseries_request, sta_requests, wfs_requests   # noqa
 from main.about import basic_site
 
 from health_check.views import get_health
@@ -28,13 +28,13 @@ urlpatterns = [
     path('gdi-backend/download/zip/<record_id>', download_requests.get_zip_download, name='zipDownload'),
     path('gdi-backend/countries', aggregation_requests.get_countries, name='countries'),
     re_path(r'^gdi-backend/proxy', views.proxy_geoserver_request, name='proxy'),
-    path('gdi-backend/sta-thing-count/<int:sta_layer_id>', sta_requests.proxy_sta_thing_count, name='sta-thing-count'),
     path('gdi-backend/sta-locations/<int:sta_layer_id>', sta_requests.proxy_sta_locations, name='sta-locations'),
     path('gdi-backend/sta-obs-properties/<int:sta_layer_id>', sta_requests.proxy_sta_obs_properties, name='sta-obs-properties'),
     path('gdi-backend/sta-thing/<int:sta_endpoint_id>/<int:thing_id>', sta_requests.proxy_sta_thing, name='sta-thing'),
     path('gdi-backend/sta-obs-properties-by-thing/<int:sta_layer_id>/<int:thing_id>', sta_requests.proxy_sta_obs_properties_by_thing, name='sta-obs-properties-by-thing'),
     path('gdi-backend/sta-datastream-properties/<int:sta_endpoint_id>/<int:thing_id>/<int:obs_property_id>', sta_requests.proxy_sta_datastream_properties, name='sta-datastream-properties'),
     path('gdi-backend/sta-all-datastreams/<int:sta_endpoint_id>/<int:thing_id>/<int:obs_property_id>', sta_requests.proxy_sta_all_datastreams, name='sta-all-datastreams'),
+    path('gdi-backend/wfs-features/<str:workspace>/<str:prefix>', wfs_requests.wfs_features, name='wfs-features'),
     re_path(r'^gdi-backend/timeseries/(?P<locale>[a-z]{2})', timeseries_request.request_all_time_series, name='request_time_series'),
     re_path(r'^gdi-backend/process', aggregation_requests.call_process, name='aggregation'),
     re_path(r'^gdi-backend/districts/(?P<country_id>[0-9][0-9])$', aggregation_requests.get_districts_by_country, name='districts'),
diff --git a/backend/main/fixtures/test_db_content.json b/backend/main/fixtures/test_db_content.json
index bb2e3e65c9cf57ab9e3fd3e4b16786caf7ee052b..1080e81cc8ffbb590ca9f7451e645f52630c19d7 100644
--- a/backend/main/fixtures/test_db_content.json
+++ b/backend/main/fixtures/test_db_content.json
@@ -19,6 +19,7 @@
       "connect_to_geoserver": true,
       "connect_to_thredds": true,
       "connect_to_sta": true,
+      "connect_to_wfs": false,
       "sta_endpoint": 1,
       "csv_parser": 1,
       "public_folder": false,
@@ -38,12 +39,14 @@
       "connect_to_geoserver": false,
       "connect_to_thredds": false,
       "connect_to_sta": false,
+      "connect_to_wfs": true,
       "sta_endpoint": null,
-      "csv_parser": null,
+      "csv_parser": 2,
       "public_folder": false,
       "quota": 100,
       "size_unit": "gi",
-      "wms_file_suffix": null
+      "wms_file_suffix": null,
+      "wfs_file_suffix": null
     }
   },
   {
@@ -101,7 +104,7 @@
       "imprint_text_en": "",
       "faq_text_de": "",
       "faq_text_en": "",
-      "theme": "blue",
+      "theme": "default",
       "default_lon": "10.4500000000",
       "default_lat": "52.0000000000",
       "default_zoom": "6.00000000",
@@ -115,7 +118,7 @@
       "viewer": null,
       "print_annotation_de": null,
       "print_annotation_en": null,
-      "base_map": "baseMap"
+      "base_layer": "baseMap"
     }
   },
   {
@@ -136,8 +139,8 @@
     "model": "main.area",
     "pk": 2,
     "fields": {
-      "name_de": "Zeitreihendaten",
-      "name_en": "Timeseries data",
+      "name_de": "Punktdaten",
+      "name_en": "Point Data",
       "project": 1,
       "position": 2,
       "is_active": true,
@@ -738,17 +741,17 @@
     "model": "main.wfslayer",
     "pk": 1,
     "fields": {
-      "name_de": "Köln",
-      "name_en": "Cologne",
+      "name_de": "Chemikalien",
+      "name_en": "Chemicals",
       "info_de": "",
       "info_en": "",
       "time_format": null,
       "y_axis_min": null,
       "y_axis_max": null,
-      "bucket": 1,
-      "wfs_url": "http://localhost:5001/gdi-backend/proxy/test-bucket/wfs",
-      "layer_name": "test-bucket:cologne_polygon",
-      "file_path": "shp/cologne_polygon_shp.zip"
+      "bucket": 2,
+      "workspace": "test-bucket",
+      "prefix": "test_bucket",
+      "file_path": "chemicals/*.csv"
     }
   },
   {
@@ -763,7 +766,24 @@
       "y_axis_min": null,
       "y_axis_max": null,
       "bucket": 1,
-      "file_path": "public/bw.json"
+      "file_path": "public/bw.json",
+      "url": "http://localhost:9000/test-bucket/public/bw.json"
+    }
+  },
+  {
+    "model": "main.geojsonlayer",
+    "pk": 2,
+    "fields": {
+      "name_de": "Köln",
+      "name_en": "Cologne",
+      "info_de": "",
+      "info_en": "",
+      "time_format": null,
+      "y_axis_min": null,
+      "y_axis_max": null,
+      "bucket": 1,
+      "file_path": "shp/cologne_polygon_shp.zip",
+      "url": "http://localhost:5001/gdi-backend/proxy/test-bucket/ows?service=WFS&version=2.0.0&request=GetFeature&typename=test-bucket:cologne_polygon&outputFormat=application/json&srsname=EPSG:3857"
     }
   },
   {
@@ -836,6 +856,20 @@
       "wfs_layer": 1
     }
   },
+  {
+    "model": "main.arealayer",
+    "pk": 6,
+    "fields": {
+      "area": 2,
+      "position": 1,
+      "is_active": true,
+      "is_default_on_start": false,
+      "wms_layer": null,
+      "sta_layer": null,
+      "geojson_layer": 2,
+      "wfs_layer": null
+    }
+  },
   {
     "model": "main.wmslayerlegend",
     "pk": 1,
@@ -1229,15 +1263,15 @@
       "record_id": "zyxwvmkcO9Ss26kiROhGIXQzvMr7L7gF",
       "publisher": 1,
       "version": "1.0",
-      "language": "en",
+      "language": null,
       "format": 3,
       "license": 1,
       "title": "Drought Monitor",
       "created_at": "2024-09-27T17:46:36.247Z",
-      "updated_at": "2024-09-30T09:41:35.295Z",
+      "updated_at": "2024-11-29T11:04:29.173Z",
       "published_at": "2024-09-30",
-      "abstract": "Test",
-      "keywords": "[\"Drought\", \"Environment\", \"Soil\"]"
+      "abstract": "test",
+      "keywords": "[\"Drought\", \"Germany\"]"
     }
   },
   {
@@ -1295,6 +1329,31 @@
       "value_col_num": 3,
       "unit_col_num": 5,
       "time_col_num": 4,
+      "location_id_col_num": null,
+      "property_id_col_num": null,
+      "country_col_num": null,
+      "community_col_num": null,
+      "target": "STA",
+      "group": 1
+    }
+  },
+  {
+    "model": "data_import.csvparser",
+    "pk": 2,
+    "fields": {
+      "lat_col_num": 25,
+      "lon_col_num": 26,
+      "station_col_num": 27,
+      "property_col_num": 2,
+      "value_col_num": 17,
+      "unit_col_num": 18,
+      "time_col_num": 21,
+      "location_id_col_num": null,
+      "property_id_col_num": 1,
+      "country_col_num": 28,
+      "community_col_num": null,
+      "target": "WFS",
+      "table_prefix": "chemicals",
       "group": 1
     }
   },
@@ -1308,6 +1367,95 @@
       "col_name": "station-id"
     }
   },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 2,
+    "fields": {
+      "parser": 2,
+      "related_entity": "thing",
+      "col_num": 4,
+      "col_name": "river_km"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 3,
+    "fields": {
+      "parser": 2,
+      "related_entity": "observation",
+      "col_num": 15,
+      "col_name": "concentration_data"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 4,
+    "fields": {
+      "parser": 2,
+      "related_entity": "observation",
+      "col_num": 16,
+      "col_name": "concentration_individual"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 5,
+    "fields": {
+      "parser": 2,
+      "related_entity": "observation",
+      "col_num": 19,
+      "col_name": "sampling_depth_type"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 6,
+    "fields": {
+      "parser": 2,
+      "related_entity": "observation",
+      "col_num": 20,
+      "col_name": "sampling_depth"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 7,
+    "fields": {
+      "parser": 2,
+      "related_entity": "observation",
+      "col_num": 22,
+      "col_name": "IDX_l"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 8,
+    "fields": {
+      "parser": 2,
+      "related_entity": "thing",
+      "col_num": 29,
+      "col_name": "water_body"
+    }
+  },
+  {
+    "model": "data_import.csvparserextracolumn",
+    "pk": 9,
+    "fields": {
+      "parser": 2,
+      "related_entity": "thing",
+      "col_num": 30,
+      "col_name": "river_basin"
+    }
+  },
+  {
+    "model": "data_import.csvincludecriteria",
+    "pk": 1,
+    "fields": {
+      "parser": 2,
+      "col_num": 31,
+      "text_value": "TRUE"
+    }
+  },
   {
     "model": "admin_interface.theme",
     "pk": 1,
diff --git a/backend/main/forms/bucket.py b/backend/main/forms/bucket.py
index d322260b20a1b5bac68e4abe9298d7a258db283b..00580497782995208a9196955d2efe01eeda6d76 100644
--- a/backend/main/forms/bucket.py
+++ b/backend/main/forms/bucket.py
@@ -99,7 +99,7 @@ class BucketAdmin(admin.ModelAdmin):
     model = Bucket
     form = BucketForm
     inlines = [BucketUserInline]
-    list_display = ('name', 'group', 'connect_to_geoserver', 'connect_to_thredds', 'connect_to_sta', 'link_to_event_logs')
+    list_display = ('name', 'group', 'connect_to_geoserver', 'connect_to_thredds', 'connect_to_sta', 'connect_to_wfs', 'link_to_event_logs')
     ordering = ('name',)
 
     fieldsets = [
@@ -107,7 +107,7 @@ class BucketAdmin(admin.ModelAdmin):
             'fields': ('name', 'group', 'public_folder', ('quota', 'size_unit'), ),
         }),
         (None, {
-            'fields': (('connect_to_geoserver', 'wms_file_suffix', 'wfs_file_suffix'), 'connect_to_thredds', ('connect_to_sta', 'sta_endpoint', 'csv_parser'), ),
+            'fields': (('connect_to_geoserver', 'wms_file_suffix', 'wfs_file_suffix'), 'connect_to_thredds', ('connect_to_sta', 'sta_endpoint'), 'connect_to_wfs', 'csv_parser', ),
         })
     ]
 
diff --git a/backend/main/forms/layer/geojson_layer.py b/backend/main/forms/layer/geojson_layer.py
index 2c966440bc593fd5d5e340311430bf2e8ae1c855..b6c7e0c518559bdd8ea114c6a09ed201c7f82fce 100644
--- a/backend/main/forms/layer/geojson_layer.py
+++ b/backend/main/forms/layer/geojson_layer.py
@@ -17,7 +17,7 @@ class GeojsonLayerAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
 
     fieldsets = [
         (None, {
-            'fields': (('name_de', 'name_en'), 'bucket', 'file_path' ),
+            'fields': (('name_de', 'name_en'), 'bucket', 'url', 'file_path' ),
             'classes': ('tab-basic',),
         }),
         (None, {
diff --git a/backend/main/forms/layer/wfs_layer.py b/backend/main/forms/layer/wfs_layer.py
index 3e56ec51e1e3d10c9c215931254086b647cc5694..5005d436416665bf2e2092255f29db3e5d940240 100644
--- a/backend/main/forms/layer/wfs_layer.py
+++ b/backend/main/forms/layer/wfs_layer.py
@@ -11,11 +11,11 @@ from django.forms import Textarea # noqa
 
 class WfsLayerAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
     model = WfsLayer
-    list_display = ('__str__', 'layer_name')
+    list_display = ('__str__', 'workspace', 'prefix')
     save_as = True
     inlines = [FkAreaLayerInline ]
 
-    search_fields = ["layer_name", "name_de", "name_en", "info_de", "info_en", "wfs_url"]
+    search_fields = ["name_de", "name_en", "info_de", "info_en", "workspace", "prefix"]
 
     formfield_overrides = {
         models.TextField: {'widget': Textarea(
@@ -29,7 +29,7 @@ class WfsLayerAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
             'classes': ('tab-basic',),
         }),
         (None, {
-            'fields': ('layer_name', 'wfs_url'),
+            'fields': ('workspace', 'prefix'),
             'classes': ('tab-wfs',),
         }),
         (None, {
@@ -46,7 +46,7 @@ class WfsLayerAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
     ]
 
     def get_readonly_fields(self, request, obj):
-        fields = ['bucket', 'wfs_url', 'layer_name', 'file_path']
+        fields = ['bucket', 'file_path']
         if request.user.is_superuser:
             return []
         return fields
diff --git a/backend/main/forms/project.py b/backend/main/forms/project.py
index c7f8a2de5c393876d8f882b26c5c715aef2ae2bf..49dfe08873541dc976388de06f142e037518c50b 100644
--- a/backend/main/forms/project.py
+++ b/backend/main/forms/project.py
@@ -28,7 +28,7 @@ class ProjectAdmin(DjangoTabbedChangeformAdmin, admin.ModelAdmin):
             'classes': ('tab-basic',),
         }),
         (None, {
-            'fields': (('show_in_gallery', 'gallery_image',  'position_in_gallery'), 'base_map',),
+            'fields': (('show_in_gallery', 'gallery_image',  'position_in_gallery'), ('base_layer', 'is_base_layer_gray'),),
             'classes': ('tab-basic',),
         }),
         ('Imprint-Page', {
diff --git a/backend/main/lib/api/GeoserverApi.py b/backend/main/lib/api/GeoserverApi.py
index 932494e91b5f80be7f45f7ef734f4ab1fdeffe2c..ddb4dbd160bc0ea2d6c05b0150e9d8b0f07819f7 100644
--- a/backend/main/lib/api/GeoserverApi.py
+++ b/backend/main/lib/api/GeoserverApi.py
@@ -1,12 +1,12 @@
 import os
 import requests
 from requests.adapters import HTTPAdapter
-from requests.packages.urllib3.util.retry import Retry
+from requests.packages.urllib3.util.retry import Retry  # noqa
+from xml.etree import ElementTree
 
 import json
 
-from geoserver.catalog import Catalog
-from owslib.wms import WebMapService
+from geoserver.catalog import Catalog  # noqa
 
 GEOSERVER_MOSAIC_DIR = "/opt/geoserver/data_dir/mosaic"
 
@@ -25,9 +25,10 @@ class GeoServerApi:
 
         self.logs = []
         self.has_error = False
+        self.has_timeout = False
 
         retry_strategy = Retry(
-            total=10,
+            total=3,
             backoff_factor=2,
             allowed_methods=['DELETE', 'GET', 'HEAD', 'OPTIONS', 'PUT', 'POST']
         )
@@ -43,6 +44,7 @@ class GeoServerApi:
     def reset(self):
         self.logs = []
         self.has_error = False
+        self.has_timeout = False
 
     def create_workspace(self, workspace_name):
         # check for existing workspace
@@ -58,7 +60,7 @@ class GeoServerApi:
         else:
             self.log("Workspace already exists. Existing Workspace will be used: {}".format(workspace_name))
 
-    def create_datastore(self, workspace, data_store, object_url, type, store_type='coveragestores'):
+    def create_datastore(self, workspace, data_store, object_url, data_type, store_type='coveragestores'):
 
         self.create_workspace(workspace)
 
@@ -67,25 +69,28 @@ class GeoServerApi:
         else:
             headers = {"Content-type": "text/plain"}
         url = '{0}workspaces/{1}/{2}/{3}/{4}'.format(
-            self.url, workspace, store_type, data_store, type)
+            self.url, workspace, store_type, data_store, data_type)
 
         try:
-            response = self.session.put(url, data=object_url, auth=(self.username, self.password), headers=headers, timeout=10)
+            response = self.session.put(url, data=object_url, auth=(self.username, self.password), headers=headers, timeout=5)
             if response.status_code != 201:
                 self.log("Create status not ok:")
                 self.log(response.content)
                 self.has_error = True
             else:
                 self.log("Datastore {} created successfully".format(data_store))
+        except requests.exceptions.RetryError:
+            self.log("Timeout, datastore could not be created: {}".format(data_store))
+            self.has_timeout = True
         except Exception as e:
-            print("Datastore could not be created: {}".format(e))
+            self.log("Error, datastore could not be created: {}".format(e))
             self.has_error = True
 
-    def _create_new_style(self, style_name, style_xml):
+    def _create_new_style(self, style_xml):
         try:
             headers = {"Content-type": "application/vnd.ogc.sld+xml"}
             url = '{0}styles?raw=true'.format(self.url)
-            response = self.session.post(url=url, auth=(self.username, self.password), headers=headers, data=style_xml, timeout=10)
+            response = self.session.post(url=url, auth=(self.username, self.password), headers=headers, data=style_xml, timeout=5)
             if response.status_code != 201:
                 print('Style could not be created: {}'.format(response.content))
                 self.has_error = True
@@ -99,7 +104,7 @@ class GeoServerApi:
         try:
             headers = {"Content-type": "application/vnd.ogc.sld+xml"}
             url = '{0}styles/{1}?raw=true'.format(self.url, style_name)
-            response = self.session.put(url=url, auth=(self.username, self.password), headers=headers, data=style_xml, timeout=10)
+            response = self.session.put(url=url, auth=(self.username, self.password), headers=headers, data=style_xml, timeout=5)
             if response.status_code != 200:
                 print('Style could not be updated: {}'.format(response.content))
                 self.has_error = True
@@ -112,13 +117,13 @@ class GeoServerApi:
     def create_style(self, style_name, style_xml):
         try:
             url = '{0}styles/{1}.sld'.format(self.url, style_name)
-            response = self.session.get(url=url, auth=(self.username, self.password), timeout=10)
+            response = self.session.get(url=url, auth=(self.username, self.password), timeout=5)
 
             style_xml = style_xml.encode('utf-8')
 
             if response.status_code != 200:
                 print('Style does not exist yet. Style will be added as new.')
-                self._create_new_style(style_name, style_xml)
+                self._create_new_style(style_xml)
             else:
                 print('Update existing style')
                 self._update_style(style_name, style_xml)
@@ -139,7 +144,7 @@ class GeoServerApi:
         }
 
         try:
-            response = self.session.post(url, data=json.dumps(body), auth=(self.username, self.password), headers=headers, timeout=10)
+            response = self.session.post(url, data=json.dumps(body), auth=(self.username, self.password), headers=headers, timeout=5)
 
             if response.status_code != 201:
                 print("Create status not ok: {}".format(response.content))
@@ -177,41 +182,46 @@ class GeoServerApi:
             "</coverage>"
         )
         try:
-            response = self.session.put(url, data=time_data, auth=(self.username, self.password), headers=headers, timeout=10)
+            response = self.session.put(url, data=time_data, auth=(self.username, self.password), headers=headers, timeout=5)
             if response.status_code not in [200, 201]:
                 self.log("Time dimension status for coverage {} not ok: {}".format(native_name, response.content))
                 self.has_error = True
             else:
                 self.log("Time dimension for coverage {} enabled successfully".format(native_name))
+        except requests.exceptions.RetryError:
+            self.log("Timeout, coverage_store could not be configured: {}".format(coverage_store))
+            self.has_timeout = True
         except Exception as e:
             self.log("Time dimension for coverage {} could not be enabled: {}".format(native_name, e))
             self.has_error = True
 
-    def update_timesteps_and_save(self, layer, wms_name):
-        wms_url = os.environ.get("GEOSERVER_URL") + '/ows?service=wms&version=1.3.0&request=GetCapabilities'
+    def get_wms_timesteps(self, workspace, layer_name):
+        wms_url = '{}/ows?service=wms&version=1.3.0&request=GetCapabilities&namespace={}'.format(os.environ.get("GEOSERVER_URL"), workspace)
 
         try:
-            wms = WebMapService(wms_url, version='1.3.0')
-
-            # iterate all available wms 'LAYERS' which exist in Geoserver
-            for wms_variable in list(wms.contents):
-                if wms_name == wms_variable:
-                    time_steps = wms.contents[wms_name].timepositions
-                    if time_steps is None:
-                        layer.time_steps = []
-                    else:
-                        layer.time_steps = time_steps
-                    break
-            layer.save()
+            response = self.session.get(wms_url)
+            xml = ElementTree.fromstring(response.text)
+
+            for item in xml.findall(".//{http://www.opengis.net/wms}Layer"):
+                if item.get("queryable") is not None:
+
+                    name = item.find('{http://www.opengis.net/wms}Name').text
+                    if name == layer_name:
+                        for dimension in item.findall("{http://www.opengis.net/wms}Dimension"):
+                            if dimension.get('name') == 'time':
+                                time_steps = dimension.text.strip()
+                                self.log("Layer-timesteps fetched")
+                                return time_steps.split(',')
+            return []
 
         except Exception as e:
-            print("Could not update layer {}: {}".format(layer.id, str(e)))
+            self.log("Could not fetch timesteps {}: {}".format(layer_name, str(e)))
             self.has_error = True
 
     def delete_datastore(self, workspace, data_store, store_type='coveragestores'):
         url = "{0}workspaces/{1}/{2}/{3}?recurse=true".format(self.url, workspace, store_type, data_store)
         try:
-            response = self.session.delete(url, auth=(self.username, self.password), timeout=10)
+            response = self.session.delete(url, auth=(self.username, self.password), timeout=5)
             if response.status_code not in [200, 201]:
                 self.log("Error in deleting datastore {}: {}".format(data_store, response.content))
                 self.has_error = True
@@ -220,3 +230,34 @@ class GeoServerApi:
         except Exception as e:
             self.log("Error in deleting datastore {}: {}".format(data_store, e))
             self.has_error = True
+
+    def add_allowed_url(self, title, allowed_url):
+
+        url = "{}urlchecks".format(self.url)
+        headers = {"content-type": "application/xml"}
+
+        # ^https://minio.ufz.de.*$
+        body = (
+            "<regexUrlCheck>"
+            "<name>" + title + "</name>"
+            "<description></description>"
+            "<enabled>true</enabled>"
+            "<regex>^" + allowed_url + ".*$</regex>"
+            "</regexUrlCheck>"
+        )
+
+        try:
+            response = self.session.post(
+                url,
+                data=body,
+                auth=(self.username, self.password),
+                headers=headers,
+                timeout=5
+            )
+            if response.status_code not in [200, 201]:
+                print("Error in setting url-check: {}".format(response.content))
+            else:
+                print("Adding URL-Check successful: {}".format(allowed_url))
+
+        except Exception as e:
+            print("Error in setting url-check: {}".format(e))
diff --git a/backend/main/lib/utils/file.py b/backend/main/lib/utils/file.py
index 3997a490568dd2fe79a2cc9117c3f6f6b13f912a..eba4cbfd7eef39fa92ef368a43d0838fb465b58b 100644
--- a/backend/main/lib/utils/file.py
+++ b/backend/main/lib/utils/file.py
@@ -40,7 +40,7 @@ def is_wms_file(bucket: Bucket, file: File):
         return True
 
 
-def is_wfs_file(bucket: Bucket, file: File):
+def is_zip_file_for_import(bucket: Bucket, file: File):
     suffix = bucket.wfs_file_suffix
     if suffix:
         for file_type in ["zip"]:
diff --git a/backend/main/management/commands/loadtestdata.py b/backend/main/management/commands/loadtestdata.py
index a496c4602fcc4b3246f2e113621ec93d1fbd339f..27f4200f69bc998e3f4ba91c5db8a9281974fa38 100644
--- a/backend/main/management/commands/loadtestdata.py
+++ b/backend/main/management/commands/loadtestdata.py
@@ -66,7 +66,14 @@ class Command(BaseCommand):
         bucket2.geonetwork_group = geonetwork_api.create_group("test-group2")
         bucket2.save()
 
-        print("\nStep 4: Import Netcdf-files")
+
+        print("\nStep 4: Configure GeoServer")
+
+        geoserver_api = GeoServerApi()
+        geoserver_api.add_allowed_url("Minio", "http://minio:9000")
+
+        print("\nStep 5: Import Netcdf-files")
+
         import_file_to_minio(BUCKET_NAME, "https://minio.ufz.de/rdm/sdi-test-data/drought.nc", "drought.nc", "drought")
         import_file_to_minio(BUCKET_NAME, "https://minio.ufz.de/rdm/sdi-test-data/precipitation.nc", "precipitation.nc", "pre")
 
@@ -74,9 +81,7 @@ class Command(BaseCommand):
         import_file_to_minio(BUCKET_NAME, "https://minio.ufz.de/rdm/sdi-test-data/pre_1950_2021_monsum.nc", "pre_1950_2021_monsum.nc", "pre_1950_2021_monsum")
 
 
-        print("\nStep 5: Configure GeoServer")
-
-        geoserver_api = GeoServerApi()
+        print("\nStep 6: Configure Layer in GeoServer")
 
         for legend in WmsLegend.objects.all():
             sld = create_sld_file(legend, "de")
@@ -100,16 +105,16 @@ class Command(BaseCommand):
                     geoserver_api.add_style(style_name_en, layer.layer_name)
 
 
-        print("\nStep 6: Upload GeoJson-File")
+        print("\nStep 7: Upload GeoJson-File")
         upload_file(BUCKET_NAME, "public/bw.json", "main/geojson/districts/08_admin.geojson")
 
-        print("\nStep 7: Upload Metadata-Json-File")
+        print("\nStep 8: Upload Metadata-Json-File")
         upload_file(BUCKET_NAME, "drought/metadata.jsonld", "main/fixtures/metadata.jsonld")
 
-        print("\nStep 8: Import STA-Data")
+        print("\nStep 9: Import STA-Data")
         import_file_to_minio(BUCKET_NAME, "https://minio.ufz.de/rdm/sdi-test-data/wis-d_input_last_14_D.csv", "wis-d_input_last_14_D.csv", "sta")
 
-        print("\nStep 9: Import Shapefile Data")
+        print("\nStep 10: Import Shapefile Data")
         import_file_to_minio(BUCKET_NAME, "https://minio.ufz.de/rdm/sdi-test-data/shp/cologne_polygon_shp.zip", "cologne_polygon_shp.zip", "shp")
 
 
diff --git a/backend/main/management/commands/parsecsv.py b/backend/main/management/commands/parsecsv.py
index cd9cefb45d09ee76874974478acb89f0ea8fe7f0..31461a0cce258719c3ad7489b34979e1611184fc 100644
--- a/backend/main/management/commands/parsecsv.py
+++ b/backend/main/management/commands/parsecsv.py
@@ -1,16 +1,17 @@
 import os
-import csv
 import time
-import datetime
 
+from django.utils import timezone   # noqa
 from django.core.management.base import BaseCommand # noqa
+from django.db.models import Q  # noqa
 
-from data_import.models import CsvParser, CsvImportJob, CsvParserExtraColumn, CsvIncludeCriteria, PointData, ExtendedPointData, STA_THING # noqa
-from main.models import StaThingProperty    # noqa
+from data_import.models import CsvImportJob, PointData, CsvIncludeCriteria # noqa
+from data_import.models import WFS, STA # noqa
 
 from main.lib.utils.s3_utils import download_minio_file # noqa
 
-from data_import.api.StaApi import StaApi    # noqa
+from data_import.lib.StaImporter import StaImporter    # noqa
+from data_import.lib.PostgisImporter import PostgisImporter # noqa
 
 
 SIZE_LIMIT = 30000000   # 30 MB
@@ -20,11 +21,12 @@ class Command(BaseCommand):
 
     def handle(self, *args, **options):
 
-        job = CsvImportJob.objects.filter(is_processed=False, is_running=False, bucket__connect_to_sta=True).select_related('bucket__sta_endpoint', 'bucket__csv_parser').order_by('created_at').first()
+        job = CsvImportJob.objects.filter(Q(is_processed=False) & Q(is_running=False) & (Q(bucket__connect_to_sta=True) | Q(bucket__connect_to_wfs=True))).select_related('bucket__sta_endpoint', 'bucket__csv_parser').order_by('created_at').first()
+
         if job:
             start_time = time.time()
 
-            job.started_at = datetime.datetime.now()
+            job.started_at = timezone.now()
             job.is_running = True
             job.save()
 
@@ -38,51 +40,31 @@ class Command(BaseCommand):
                 job.save()
                 return
 
-            with open(file_name, newline = '') as csvfile:
-
-                reader = csv.reader(csvfile, delimiter=',')
-                next(reader)  # skip header
-
-                api = StaApi(job)
-                thing_props = StaThingProperty.objects.filter(endpoint=job.bucket.sta_endpoint)
-                extra_columns = CsvParserExtraColumn.objects.filter(parser=job.bucket.csv_parser)
-
-                include_criteria = CsvIncludeCriteria.objects.filter(parser=job.bucket.csv_parser)
-
-                rows_succeeded = 0
-                rows_failed = 0
-                for row in reader:
-
-                    if is_row_included(row, include_criteria):
-
-                        point_data = create_point_data(job, row)
-                        extended_data = create_extended_data(point_data, extra_columns, thing_props, row)
-
-                        api.import_point_data(point_data, extended_data)
-
-                        if api.error is True:
-                            point_data.validation_error = "\n".join(api.logs)
-                            point_data.save()
-                            for data in extended_data:
-                                data.save()
-                            rows_failed += 1
-                        else:
-                            rows_succeeded += 1
-                        api.error = False
-                        api.logs = []
+            if job.target == STA:
+                i = StaImporter(job)
+                i.import_csv_in_sta()
 
                 if not PointData.objects.filter(import_job=job).exists():
                     job.is_success = True
 
-                job.is_processed = True
-                job.is_running = False
-                job.execution_time = str(round((time.time() - start_time), 2)) + "s"
-                job.finished_at = datetime.datetime.now()
-                job.data_points_created = rows_succeeded
-                job.data_points_failed = rows_failed
-                job.save()
+            if job.target == WFS:
+                i = PostgisImporter(job)
+                i.import_csv_to_wfs()
+                i.write_data()
+                job.is_success = not i.error
+                if i.error is True:
+                    job.validation_error = "\n".join(i.logs)
+
+            job.is_processed = True
+            job.is_running = False
+            job.execution_time = str(round((time.time() - start_time), 2)) + "s"
+            job.finished_at = timezone.now()
+            #job.data_points_created = rows_succeeded
+            #job.data_points_failed = rows_failed
+            job.save()
+
+            os.remove(file_name)
 
-                os.remove(file_name)
 
 def set_file_stats_and_validate(job: CsvImportJob, file_name: str):
     job.file_size = os.path.getsize(file_name)
@@ -94,55 +76,3 @@ def set_file_stats_and_validate(job: CsvImportJob, file_name: str):
 
     if job.num_rows > ROW_LIMIT:
         job.validation_error = 'file exceeds maximum number of rows: {}'.format(ROW_LIMIT)
-
-
-def is_row_included(row, include_criteria: list[CsvIncludeCriteria]) -> bool:
-    if len(include_criteria) > 0:
-        for criteria in include_criteria:
-            if row[criteria.col_num] == criteria.text_value:
-                return True
-        return False
-    else:
-        return True
-
-
-def create_point_data(job: CsvImportJob, row):
-    p: CsvParser = job.bucket.csv_parser
-
-    point_data = PointData(
-        import_job = job,
-        thing_name = row[p.station_col_num],
-        location_name = row[p.station_col_num],
-        coord_lat = row[p.lat_col_num],
-        coord_lon = row[p.lon_col_num],
-        # geometry = '',
-        property = row[p.property_col_num],
-        # sensor = '',
-        result_value = row[p.value_col_num],
-        result_unit = row[p.unit_col_num],
-        result_time = row[p.time_col_num]
-    )
-    return point_data
-
-def create_extended_data(point_data: PointData, extra_columns: list[CsvParserExtraColumn], thing_props: list[StaThingProperty], row):
-    result = []
-
-    for prop in thing_props:
-        extended_data = ExtendedPointData(
-            point_data = point_data,
-            related_entity = STA_THING,
-            name = prop.property_key,
-            value = prop.property_value
-        )
-        result.append(extended_data)
-
-    for column in extra_columns:
-        extended_data = ExtendedPointData(
-            point_data = point_data,
-            related_entity = column.related_entity,
-            name = column.col_name,
-            value = row[column.col_num]
-        )
-        result.append(extended_data)
-
-    return result
diff --git a/backend/main/management/commands/work.py b/backend/main/management/commands/work.py
index 78c44656fa63e72b324fcef28b82c2c5660adbfc..0d599f8d1c36b0956f0d99e33eb7540c3caf0c4a 100644
--- a/backend/main/management/commands/work.py
+++ b/backend/main/management/commands/work.py
@@ -8,13 +8,13 @@ from minio import Minio # noqa
 
 from metadata.lib import MetadataService, JsonLDParser # noqa
 from main.lib.utils import NetcdfParser # noqa
-from main.lib.utils.file import File, is_wms_file, is_wfs_file, create_file_identifier, get_or_create_metadata_record, get_metadata_record, search_metadata_record # noqa
+from main.lib.utils.file import File, is_wms_file, is_zip_file_for_import, create_file_identifier, get_or_create_metadata_record, get_metadata_record, search_metadata_record # noqa
 from main.lib.utils.backend import NETCDF_HELPER_VARIABLES, TIME_BNDS # noqa
 
 
-from main.models import Bucket, BucketEvent, WmsLayer, WfsLayer, DataVariable # noqa
+from main.models import Bucket, BucketEvent, WmsLayer, GeojsonLayer, WfsLayer, DataVariable # noqa
 from metadata.models import MetadataRecord # noqa
-from data_import.models import CsvImportJob, PointData # noqa
+from data_import.models import CsvImportJob, WFS, STA # noqa
 
 from main.lib.utils.s3_utils import download_minio_file, get_s3_filesystem, get_minio_client # noqa
 from main.lib.api.GeoserverApi import GeoServerApi # noqa
@@ -62,17 +62,19 @@ class Command(BaseCommand):
                                     self.create_netcdf_datastore(bucket, file)
                                 if file.is_geotiff():
                                     self.create_geotiff_datastore(bucket, file)
-                            if is_wfs_file(bucket, file):
+                            if is_zip_file_for_import(bucket, file):
                                 if file.is_shapefile():
                                     self.create_shapefile_datastore(bucket, file)
                         if bucket.connect_to_thredds:
                             if file.is_netcdf():
                                 self.download_to_thredds(file)
-                        if bucket.connect_to_sta:
-                            if file.is_csv():
+                        if file.is_csv():
+                            if bucket.connect_to_sta:
                                 previous_jobs = get_finished_import_jobs(bucket, file)
                                 delete_sta_data(previous_jobs)
-                                create_import_job(bucket, file)
+                                create_import_job(bucket, file, STA)
+                            if bucket.connect_to_wfs:
+                                create_import_job(bucket, file, WFS)
 
                     if event.is_delete_event():
 
@@ -84,8 +86,10 @@ class Command(BaseCommand):
                         if bucket.connect_to_geoserver:
                             if is_wms_file(bucket, file):
                                 self.delete_wms_datastore(file)
-                            if is_wfs_file(bucket, file):
-                                self.delete_wfs_datastore(file)
+                            if is_zip_file_for_import(bucket, file):
+                                # TODO
+                                # self.delete_shp_wfs_datastore(file)
+                                print('deleting of shp wfs not implemented yet')
                         if bucket.connect_to_thredds:
                             if file.is_netcdf():
                                 self.remove_from_thredds(file.absolute_path)
@@ -160,9 +164,10 @@ class Command(BaseCommand):
 
         for variable in get_netcdf_variables(file, bucket):
 
-            wms_name = workspace + ':' + coverage_store + '_' + variable.name
+            datastore_name = coverage_store + '_' + variable.name
+            layer_name = workspace + ':' + datastore_name
 
-            layers = list(WmsLayer.objects.filter(layer_name=wms_name, variable=variable))
+            layers = list(WmsLayer.objects.filter(layer_name=layer_name, variable=variable))
 
             if len(layers) == 0:
                 backend_proxy_url = os.environ.get("BACKEND_URL") + '/proxy/' + workspace + '/wms'
@@ -174,7 +179,7 @@ class Command(BaseCommand):
                         axis_label_x='Long',
                         axis_label_y='Lat',
                         scale_factor=1,
-                        layer_name=wms_name,
+                        layer_name=layer_name,
                         variable=variable,
                         no_data_value=-9999,
                         file_path=file.absolute_path,
@@ -187,7 +192,8 @@ class Command(BaseCommand):
                 self.geoserver_api.configure_layer(workspace, coverage_store, native_name)
 
             for layer in layers:
-                self.geoserver_api.update_timesteps_and_save(layer, wms_name)
+                layer.time_steps = self.geoserver_api.get_wms_timesteps(workspace, layer_name)
+                layer.save()
 
         self.logs.extend(self.geoserver_api.logs)
         self.has_error = self.geoserver_api.has_error
@@ -236,27 +242,29 @@ class Command(BaseCommand):
 
         object_url = minio_client.presigned_get_object(bucket.name, file.relative_path)
         workspace = bucket.name
-        data_store = create_wfs_name(file)
+        data_store = create_shp_wfs_name(file)
         if bucket.wfs_file_suffix:
             data_store = data_store.removesuffix(bucket.wfs_file_suffix)
             data_store = data_store.rstrip('_.- ')
 
         self.geoserver_api.create_datastore(workspace, data_store, object_url, 'url.shp', 'datastores')
 
-        wfs_name = workspace + ':' + data_store
+        layer_name = workspace + ':' + data_store
+
+        base_url = os.environ.get("BACKEND_URL") + '/proxy/' + workspace + '/ows'
+
+        url = '{}?service=WFS&version=2.0.0&request=GetFeature&typename={}&outputFormat=application/json&srsname=EPSG:3857'.format(base_url, layer_name)
 
         try:
-            wfs_layer = WfsLayer.objects.get(layer_name=wfs_name)
-        except WfsLayer.DoesNotExist:
-            backend_proxy_url = os.environ.get("BACKEND_URL") + '/proxy/' + workspace + '/wfs'
-            wfs_layer = WfsLayer(
+            geojson_layer = GeojsonLayer.objects.get(url=url)
+        except GeojsonLayer.DoesNotExist:
+            geojson_layer = GeojsonLayer(
                 bucket=bucket,
-                wfs_url=backend_proxy_url,
-                layer_name=wfs_name,
+                url=url,
                 file_path=file.relative_path,
             )
 
-        wfs_layer.save()
+        geojson_layer.save()
 
         self.logs.extend(self.geoserver_api.logs)
         self.has_error = self.geoserver_api.has_error
@@ -285,10 +293,10 @@ class Command(BaseCommand):
                 layer.save()
 
 
-    def delete_wfs_datastore(self, file: File):
+    def delete_shp_wfs_datastore(self, file: File):
 
         try:
-            layer = WfsLayer.objects.get(file_path=file.absolute_path)
+            layer = GeojsonLayer.objects.get(file_path=file.absolute_path)
 
             pieces = layer.layer_name.split(':')
             workspace = pieces[0]
@@ -335,7 +343,9 @@ class Command(BaseCommand):
 
         event.logs = final_logs
         event.is_success = not self.has_error
-        event.is_new = False
+
+        if not self.geoserver_api.has_timeout:
+            event.is_new = False
         event.save()
 
 
@@ -356,11 +366,13 @@ def create_data_store_name(file: File) -> str:
     return result.replace(".", "_")
 
 
-def create_wfs_name(file: File) -> str:
+def create_shp_wfs_name(file: File) -> str:
     name_parts = file.relative_path.split('/')
     file_name = name_parts[-1]
     name = file_name.split('.')[0]
-    return name.removesuffix('_zip')
+    del name_parts[-1]
+    name_parts.append(name.removesuffix('_zip'))
+    return '_'.join(name_parts)
 
 
 def get_crs(file: File):
@@ -402,10 +414,11 @@ def get_or_create_variable(variable_name: str, bucket: Bucket):
     return data_variable
 
 
-def create_import_job(bucket: Bucket, file: File):
+def create_import_job(bucket: Bucket, file: File, target: str):
     job = CsvImportJob(
         bucket=bucket,
         s3_file=file.relative_path,
+        target=target
     )
     job.save()
 
diff --git a/backend/main/migrations/0038_bucket_connect_to_wfs.py b/backend/main/migrations/0038_bucket_connect_to_wfs.py
new file mode 100644
index 0000000000000000000000000000000000000000..7136d6b9b430d1e8eab88d00335469a9b258dc76
--- /dev/null
+++ b/backend/main/migrations/0038_bucket_connect_to_wfs.py
@@ -0,0 +1,18 @@
+# Generated by Django 4.2.16 on 2024-11-15 10:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0037_bucket_wfs_file_suffix_wfslayer_arealayer_wfs_layer'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='bucket',
+            name='connect_to_wfs',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/backend/main/migrations/0039_wfslayer_cql_filter_for_locations_and_more.py b/backend/main/migrations/0039_wfslayer_cql_filter_for_locations_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..d115ea4a5e6459712ed98e8309eb0aa2a9d06fe2
--- /dev/null
+++ b/backend/main/migrations/0039_wfslayer_cql_filter_for_locations_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.16 on 2024-11-26 06:57
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0038_bucket_connect_to_wfs'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='wfslayer',
+            name='cql_filter_for_locations',
+            field=models.CharField(blank=True, max_length=4000, null=True),
+        ),
+        migrations.AddField(
+            model_name='wfslayer',
+            name='cql_filter_for_properties',
+            field=models.CharField(blank=True, max_length=4000, null=True),
+        ),
+    ]
diff --git a/backend/main/migrations/0040_remove_wfslayer_cql_filter_for_locations_and_more.py b/backend/main/migrations/0040_remove_wfslayer_cql_filter_for_locations_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..80aeb86c5e5a7886fef3d3628a37c49eeb0fa988
--- /dev/null
+++ b/backend/main/migrations/0040_remove_wfslayer_cql_filter_for_locations_and_more.py
@@ -0,0 +1,52 @@
+# Generated by Django 4.2.16 on 2024-11-29 07:32
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0039_wfslayer_cql_filter_for_locations_and_more'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='wfslayer',
+            name='cql_filter_for_locations',
+        ),
+        migrations.RemoveField(
+            model_name='wfslayer',
+            name='cql_filter_for_properties',
+        ),
+        migrations.RemoveField(
+            model_name='wfslayer',
+            name='layer_name',
+        ),
+        migrations.RemoveField(
+            model_name='wfslayer',
+            name='wfs_url',
+        ),
+        migrations.AddField(
+            model_name='geojsonlayer',
+            name='url',
+            field=models.CharField(default='', max_length=4000),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='wfslayer',
+            name='prefix',
+            field=models.CharField(default='', max_length=1000),
+            preserve_default=False,
+        ),
+        migrations.AddField(
+            model_name='wfslayer',
+            name='workspace',
+            field=models.CharField(default='', max_length=1000),
+            preserve_default=False,
+        ),
+        migrations.AlterField(
+            model_name='wfslayer',
+            name='file_path',
+            field=models.CharField(blank=True, max_length=4000, null=True),
+        ),
+    ]
diff --git a/backend/main/migrations/0041_rename_base_map_project_base_layer_and_more.py b/backend/main/migrations/0041_rename_base_map_project_base_layer_and_more.py
new file mode 100644
index 0000000000000000000000000000000000000000..eaaf1df66492d8c8fde151d5918dde90abafb924
--- /dev/null
+++ b/backend/main/migrations/0041_rename_base_map_project_base_layer_and_more.py
@@ -0,0 +1,23 @@
+# Generated by Django 4.2.16 on 2025-01-07 13:53
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('main', '0040_remove_wfslayer_cql_filter_for_locations_and_more'),
+    ]
+
+    operations = [
+        migrations.RenameField(
+            model_name='project',
+            old_name='base_map',
+            new_name='base_layer',
+        ),
+        migrations.AddField(
+            model_name='project',
+            name='is_base_layer_gray',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/backend/main/models.py b/backend/main/models.py
index 3c048d8ff9f2b3e343d24a212e1894c1c181fc6b..20e703e4b66ba5f5f5f81f311f7926cceaea21c8 100644
--- a/backend/main/models.py
+++ b/backend/main/models.py
@@ -3,7 +3,6 @@ from django.contrib.postgres.fields import ArrayField    # noqa
 from django.contrib.auth.models import Group, User    # noqa
 from colorfield.fields import ColorField    # noqa
 import re
-import datetime
 from urllib.parse import unquote
 
 from data_import.models import CsvParser    # noqa
@@ -61,8 +60,8 @@ REQUEST_TYPES = (
 def validate_bucket_name(value):
     if len(value) < 3 or len(value) > 63:
         raise ValidationError("Length must be between 3 and 63 characters.")
-    if not re.match(r'^[a-z0-9-]*$', value):
-        raise ValidationError('Only lowercase-letters, numbers and hyphens are allowed.')
+    if not re.match(r'^[a-z-]*$', value):
+        raise ValidationError('Only lowercase-letters and hyphens are allowed.')
     if "--" in value:
         raise ValidationError("Invalid combination of characters: '--'")
     if value.startswith('xn--'):
@@ -107,6 +106,7 @@ class Bucket(models.Model):
     connect_to_geoserver = models.BooleanField(default=False)
     connect_to_thredds = models.BooleanField(default=False)
     connect_to_sta = models.BooleanField(default=False)
+    connect_to_wfs = models.BooleanField(default=False)
     sta_endpoint = models.ForeignKey(StaEndpoint, on_delete=models.CASCADE, blank=True, null=True)
     csv_parser = models.ForeignKey(CsvParser, on_delete=models.CASCADE, blank=True, null=True)
     public_folder = models.BooleanField(default=False)
@@ -169,7 +169,8 @@ class Project(CommonInfo):
     viewer = models.CharField(max_length=4000, blank=True, null=True)
     print_annotation_de = models.CharField(max_length=120, blank=True, null=True)
     print_annotation_en = models.CharField(max_length=120, blank=True, null=True)
-    base_map = models.CharField(max_length=20, choices=BASE_MAPS, default='osm')
+    base_layer = models.CharField(max_length=20, choices=BASE_MAPS, default='osm')
+    is_base_layer_gray = models.BooleanField(default=False)
 
 
 class Area(CommonInfo):
@@ -323,9 +324,9 @@ class WmsLayer(Layer):
 
 class WfsLayer(Layer):
     bucket = models.ForeignKey(Bucket, on_delete=models.CASCADE)  # is readonly
-    wfs_url = models.URLField(max_length=4000, null=True)  # is readonly
-    layer_name = models.CharField(max_length=4000)  # is readonly
-    file_path = models.CharField(max_length=4000)  # is readonly
+    workspace = models.CharField(max_length=1000)
+    prefix = models.CharField(max_length=1000)
+    file_path = models.CharField(max_length=4000, blank=True, null=True)
 
     def __str__(self):
         if self.name_en:
@@ -341,6 +342,7 @@ class WfsLayer(Layer):
 class GeojsonLayer(Layer):
     bucket = models.ForeignKey(Bucket, on_delete=models.CASCADE, null=True)
     file_path = models.CharField(max_length=4000, null=True)
+    url = models.CharField(max_length=4000)
 
     class Meta:
         verbose_name = "GeoJSON-Data-Layer"
diff --git a/backend/main/views/sta_requests.py b/backend/main/views/sta_requests.py
index 29150addd83da3946aa009cd4e44ef7be4e595c8..7a5debe326980edfabe6f8e3a1fd4d6aa97e77d7 100644
--- a/backend/main/views/sta_requests.py
+++ b/backend/main/views/sta_requests.py
@@ -1,45 +1,9 @@
 import requests
 
-from django.http import HttpResponse
-from django.shortcuts import get_object_or_404
+from django.http import HttpResponse  # noqa
+from django.shortcuts import get_object_or_404  # noqa
 
-from main.models import StaEndpoint, StaLayer, StaThingProperty, StaObservedPropertyFilter
-
-
-def proxy_sta_thing_count(request, sta_layer_id: int):
-
-    url = "v1.1/Things?"
-
-    filter_params = _get_datastream_filter_params(request)
-
-    sta_layer = get_object_or_404(StaLayer, id=sta_layer_id)
-    endpoint = sta_layer.endpoint
-    for thing_filter in _get_filter_for_things(endpoint, ''):
-        filter_params.append(thing_filter)
-
-    url = url + "$expand=Locations($select=location)"
-
-    op_filters = _get_filter_for_observed_properties(sta_layer, 'Datastreams/ObservedProperty/')
-    for op_filter in op_filters:
-        filter_params.append(op_filter)
-
-    url = url + "&$count=true&$select=id&$top=1"
-
-    # try to make request with selecting locations by Bounding-Box
-    advanced_filter_params = filter_params + _get_bbox_filter_params(request)
-    advanced_url = url + "&$filter=" + " and ".join(advanced_filter_params)
-
-    advanced_url = endpoint.base_url + advanced_url
-    response = requests.get(advanced_url, auth=(endpoint.username, endpoint.password), timeout=(5, 60))
-
-    if response.status_code == 500:
-        # if Bounding-Box is not supported by STA-Endpoint use only the other filters
-        standard_url = url
-        if len(filter_params) > 0:
-          standard_url = url + "&$filter=" + " and ".join(filter_params)
-        return _request_sta(standard_url, endpoint)
-
-    return HttpResponse(response, content_type='application/json')
+from main.models import StaEndpoint, StaLayer, StaThingProperty, StaObservedPropertyFilter  # noqa
 
 
 def proxy_sta_locations(request, sta_layer_id: int):
@@ -55,7 +19,7 @@ def proxy_sta_locations(request, sta_layer_id: int):
 
     url = url + "$expand=Locations($select=location)"
 
-    obs_property_id = request.GET.get('obsProperty')
+    obs_property_id = request.GET.get('property')
     if obs_property_id:
         # continue "expand"
         url = url + ",Datastreams($filter=ObservedProperty/id eq {};$top=1;$orderBy=resultTime desc;$expand=Observations($select=result;$top=1;$orderBy=resultTime desc))".format(obs_property_id)
@@ -64,7 +28,11 @@ def proxy_sta_locations(request, sta_layer_id: int):
         for op_filter in op_filters:
             filter_params.append(op_filter)
 
-    url = url + "&$count=true&$select=id&$resultFormat=GeoJSON&$orderBy=Locations/location/coordinates/0,Locations/location/coordinates/1"
+    max_results = request.GET.get('count')
+    if max_results:
+        url = url + "&$count=true&$select=id&$top=" + str(max_results)
+    else:
+        url = url + "&$count=true&$select=id&$resultFormat=GeoJSON&$orderBy=Locations/location/coordinates/0,Locations/location/coordinates/1"
 
     # try to make request with selecting locations by Bounding-Box
     advanced_filter_params = filter_params + _get_bbox_filter_params(request)
@@ -170,7 +138,7 @@ def _get_datastream_filter_params(request):
 
     filter_params = []
 
-    obs_property_id = request.GET.get('obsProperty')
+    obs_property_id = request.GET.get('property')
     if obs_property_id:
         param = "Datastreams/ObservedProperty/id eq {}".format(obs_property_id)
         filter_params.append(param)
@@ -197,22 +165,28 @@ def _get_bbox_filter_params(request):
 
     filter_params = []
 
-    bbox_top_lon = request.GET.get('bboxTopLon')
-    bbox_top_lat = request.GET.get('bboxTopLat')
-    bbox_bottom_lon = request.GET.get('bboxBottomLon')
-    bbox_bottom_lat = request.GET.get('bboxBottomLat')
+    # bottomLeftLon, bottomLeftLat, topRightLon, topRightLat
+    bbox_params = request.GET.get('bbox')
+
+    if bbox_params:
+        bbox_params = bbox_params.split(',')
+
+        bbox_top_lon = bbox_params[0]
+        bbox_top_lat = bbox_params[3]
+        bbox_bottom_lon = bbox_params[2]
+        bbox_bottom_lat = bbox_params[1]
 
-    if bbox_top_lon and bbox_top_lat and bbox_bottom_lon and bbox_bottom_lat:
+        if bbox_top_lon and bbox_top_lat and bbox_bottom_lon and bbox_bottom_lat:
 
-        # define polygon in WKT-representation
-        b_r_corner = "{} {}".format(bbox_bottom_lon, bbox_bottom_lat)
-        t_r_corner = "{} {}".format(bbox_bottom_lon, bbox_top_lat)
-        t_l_corner = "{} {}".format(bbox_top_lon, bbox_top_lat)
-        b_l_corner = "{} {}".format(bbox_top_lon, bbox_bottom_lat)
+            # define polygon in WKT-representation
+            b_r_corner = "{} {}".format(bbox_bottom_lon, bbox_bottom_lat)
+            t_r_corner = "{} {}".format(bbox_bottom_lon, bbox_top_lat)
+            t_l_corner = "{} {}".format(bbox_top_lon, bbox_top_lat)
+            b_l_corner = "{} {}".format(bbox_top_lon, bbox_bottom_lat)
 
-        # define counter-clock-wise with coords as lon-lat
-        polygon = "{}, {}, {}, {}, {}".format(b_r_corner, t_r_corner, t_l_corner, b_l_corner, b_r_corner)
+            # define counter-clock-wise with coords as lon-lat
+            polygon = "{}, {}, {}, {}, {}".format(b_r_corner, t_r_corner, t_l_corner, b_l_corner, b_r_corner)
 
-        filter_params.append("st_within(Locations/location, geography'POLYGON (({}))')".format(polygon))
+            filter_params.append("st_within(Locations/location, geography'POLYGON (({}))')".format(polygon))
 
     return filter_params
\ No newline at end of file
diff --git a/backend/main/views/timeseries_request.py b/backend/main/views/timeseries_request.py
index 7bd9518a6eba6619cf1714b1300281fdb7096a12..3acf9d3dae4e96097e0fa16944e51afdc999684a 100644
--- a/backend/main/views/timeseries_request.py
+++ b/backend/main/views/timeseries_request.py
@@ -9,7 +9,7 @@ from django.shortcuts import get_object_or_404    # noqa
 from pyproj import Transformer    # noqa
 from main.models import AreaLayer, WmsLayer, WmsLayerLegend, WmsColor, ConnectedLayer, REQUEST_TYPE_1, REQUEST_TYPE_2    # noqa
 
-from main.views import views, sta_requests    # noqa
+from main.views import views, sta_requests, wfs_requests    # noqa
 
 from multiprocessing import Pool
 
@@ -50,6 +50,16 @@ def request_all_time_series(request, locale: str):
 
         chart_name = timeseries['name']
 
+
+    # collect WFS-Data
+
+    wfs_workspace = request.GET.get('workspace')
+    wfs_prefix = request.GET.get('prefix')
+    wfs_timeseries_id = request.GET.get('wfsTimeSeries')
+    if wfs_timeseries_id:
+        timeseries = _request_wfs_timeseries(wfs_workspace, wfs_prefix, wfs_timeseries_id)
+        timeseries_collection.append(timeseries)
+
     # collect WMS-Data
 
     wms_layer_id = request.GET.get('wmsLayer')
@@ -333,18 +343,48 @@ def _request_sta_timeseries(request, area_layer, sta_layer, thing_id, obs_proper
     return time_series
 
 
+def _request_wfs_timeseries(workspace, prefix, timeseries_id):
+
+    unit = ''
+
+    response = wfs_requests.get_time_records(workspace, prefix, timeseries_id)
+
+    time_records = json.loads(response.content)
+
+    time_series = {
+        'x': [],
+        'y': [],
+        'type': 'scatter',
+        'mode': 'lines+markers',
+        'name': 'wfs',
+        'position': 0,
+        'timeFormat': None,
+        'unit': unit,
+        'yMin': None,
+        'yMax': None,
+        'visible': True
+    }
+
+    for time_record in time_records['features']:
+        time_series['x'].append(time_record['properties']['date'])
+        time_series['y'].append(time_record['properties']['value_number'])
+
+    return time_series
+
+
 def _get_name_for_sta_data(obs_property, locale):
     name = ''
 
     if obs_property['name']:
         name = obs_property['name']
 
-    if locale == "de":
-        if 'name_de' in obs_property['properties']:
-            name = obs_property['properties']['name_de']
-    else:
-        if obs_property['properties']['name_en']:
-            name = obs_property['properties']['name_en']
+    if 'properties' in obs_property:
+        if locale == "de":
+            if 'name_de' in obs_property['properties']:
+                name = obs_property['properties']['name_de']
+        else:
+            if obs_property['properties']['name_en']:
+                name = obs_property['properties']['name_en']
 
     return name
 
diff --git a/backend/main/views/views.py b/backend/main/views/views.py
index c1a8e4e6bd2715fbc3ef9fddb17d29e564ed6fa6..512071b127da0ea5e8a742e0775b7f85c5260438 100644
--- a/backend/main/views/views.py
+++ b/backend/main/views/views.py
@@ -201,17 +201,17 @@ def get_geojson_layer(area_layer: AreaLayer, locale: str):
     layer_record = set_text_by_locale(layer_record, locale)
     layer_record = set_text_by_locale(layer_record, locale, 'info')
 
-    base_url = os.environ.get('MINIO_ENDPOINT')
-    if base_url.endswith('/'):
-        base_url = base_url.rstrip('/')
-    if base_url == 'http://minio:9000':
-        base_url = 'http://localhost:9000'
-
-    file_path = geojson_layer.file_path
-    if file_path.startswith('/'):
-        file_path = file_path.lstrip('/')
-
-    layer_record['url'] = '{}/{}/{}'.format(base_url, geojson_layer.bucket.name, file_path)
+    # base_url = os.environ.get('MINIO_ENDPOINT')
+    # if base_url.endswith('/'):
+    #     base_url = base_url.rstrip('/')
+    # if base_url == 'http://minio:9000':
+    #     base_url = 'http://localhost:9000'
+    #
+    # file_path = geojson_layer.file_path
+    # if file_path.startswith('/'):
+    #     file_path = file_path.lstrip('/')
+    #
+    # layer_record['url'] = '{}/{}/{}'.format(base_url, geojson_layer.bucket.name, file_path)
 
     layer_record['id'] = area_layer.id
     layer_record['type'] = 'geojson'
@@ -227,11 +227,9 @@ def get_wfs_layer(area_layer: AreaLayer, locale: str):
     layer_record = set_text_by_locale(layer_record, locale)
     layer_record = set_text_by_locale(layer_record, locale, 'info')
 
-    url = '{}?service=WFS&version=1.1.0&request=GetFeature&typename={}&outputFormat=application/json&srsname=EPSG:3857'.format(layer_record['wfs_url'], layer_record['layer_name'])
-    layer_record['url'] = url
-
     layer_record['id'] = area_layer.id
-    layer_record['type'] = 'geojson'
+    layer_record['own_id'] = wfs_layer.id
+    layer_record['type'] = 'wfs'
 
     return layer_record
 
diff --git a/backend/main/views/wfs_requests.py b/backend/main/views/wfs_requests.py
new file mode 100644
index 0000000000000000000000000000000000000000..f5ea2cc899f9ceb1c82b241b90b260dc42a5df9f
--- /dev/null
+++ b/backend/main/views/wfs_requests.py
@@ -0,0 +1,178 @@
+import os
+import requests
+from jinja2 import Template
+
+from django.http import HttpResponse, JsonResponse    # noqa
+
+
+def get_time_records(workspace: str, prefix: str, timeseries_id: int):
+
+    url = """
+        http://localhost:8000/gdi-backend/proxy/geoserver/{workspace}/ows?service=WFS
+        &version=2.0.0
+        &request=GetFeature
+        &typeName={workspace}%3A{prefix}_timerecord
+        &outputFormat=application%2Fjson
+        &CQL_FILTER=timeseries_id%20=%20{timeseries_id}
+        &propertyName=value_number,date
+        &sortBy=date+A
+    """.format(workspace=workspace, prefix=prefix, timeseries_id=timeseries_id)
+
+    return requests.get(url, timeout=(5, 60))
+
+
+def wfs_features(request, workspace, prefix):
+
+        data = _create_request_body(request, workspace, prefix)
+
+        url = '{}/{}/ows?service=WFS&version=2.0.0&outputFormat=application%2Fjson&count=1'.format(os.environ.get("GEOSERVER_URL"), workspace)
+
+        response = requests.post(url, data=data)
+        return HttpResponse(response, content_type='application/json')
+
+
+def _create_request_body(request, workspace, prefix):
+
+    count = request.GET.get('count')
+    # bottomLeftLon, bottomLeftLat, topRightLon, topRightLat
+    bbox = request.GET.get('bbox')
+    coords = bbox.split(',')
+
+    property_id = request.GET.get('property')
+    begin_date = request.GET.get('beginDate')
+    end_date = request.GET.get('endDate')
+    threshold_value = request.GET.get('threshold')
+
+    if property_id:
+
+        filter_xml = """<?xml version="1.0" encoding="UTF-8"?>
+            <wfs:GetFeature service="WFS" version="2.0.0" count="{{ count|e }}"
+                xmlns:wfs="http://www.opengis.net/wfs/2.0"
+                xmlns:fes="http://www.opengis.net/fes/2.0"
+                xmlns:gml="http://www.opengis.net/gml/3.2"
+                xmlns:sf="http://www.openplans.org/spearfish"
+                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                xsi:schemaLocation="http://www.opengis.net/wfs/2.0
+                                    http://schemas.opengis.net/wfs/2.0/wfs.xsd
+                                    http://www.opengis.net/gml/3.2
+                                    http://schemas.opengis.net/gml/3.2.1/gml.xsd">
+                <wfs:Query typeNames="{{ workspace|e }}:{{ prefix|e }}_feature {{ workspace|e }}:{{ prefix|e }}_timeseries" aliases="a b">
+        
+                  <fes:Filter>
+                     <fes:And>
+                        
+                        <fes:BBOX>
+                           <fes:ValueReference>a/geom</fes:ValueReference>
+                           <gml:Envelope srsName="http://www.opengis.net/def/crs/epsg/0/4326">
+                              <gml:lowerCorner>{{ lowerCornerLat|e }} {{ lowerCornerLon|e }}</gml:lowerCorner>
+                              <gml:upperCorner>{{ upperCornerLat|e }} {{ upperCornerLon|e }}</gml:upperCorner>
+                           </gml:Envelope>
+                        </fes:BBOX>
+                        
+                        <fes:PropertyIsEqualTo>
+                           <fes:ValueReference>a/id</fes:ValueReference>
+                           <fes:ValueReference>b/feature_id</fes:ValueReference>
+                        </fes:PropertyIsEqualTo>
+                        
+                        {% if property_id %}
+                            <fes:PropertyIsEqualTo>
+                               <fes:ValueReference>b/property_id</fes:ValueReference>
+                               <fes:Literal>{{ property_id|e }}</fes:Literal>
+                            </fes:PropertyIsEqualTo>
+                        {% endif %}
+                        
+                        {% if begin_date %}
+                           <fes:PropertyIsGreaterThanOrEqualTo>
+                                <fes:ValueReference>b/max_date</fes:ValueReference>
+                                <fes:Function name="dateParse">
+                                    <fes:Literal>yyyy-MM-dd</fes:Literal>
+                                    <fes:Literal>{{ begin_date|e }}</fes:Literal>
+                                </fes:Function>
+                           </fes:PropertyIsGreaterThanOrEqualTo>
+                        {% endif %}
+
+                        {% if end_date %}
+                           <fes:PropertyIsLessThanOrEqualTo>
+                                <fes:ValueReference>b/min_date</fes:ValueReference>
+                                <fes:Function name="dateParse">
+                                    <fes:Literal>yyyy-MM-dd</fes:Literal>
+                                    <fes:Literal>{{ end_date|e }}</fes:Literal>
+                                </fes:Function>
+                           </fes:PropertyIsLessThanOrEqualTo>
+                        {% endif %}
+                        
+                        {% if threshold_value %}
+                           <fes:PropertyIsGreaterThanOrEqualTo>
+                                <fes:ValueReference>b/max_value</fes:ValueReference>
+                                <fes:Literal>{{ threshold_value|e }}</fes:Literal>
+                           </fes:PropertyIsGreaterThanOrEqualTo>
+                        {% endif %}
+                        
+                     </fes:And>
+                  </fes:Filter>
+                </wfs:Query>
+            </wfs:GetFeature>"""
+
+    else:
+        filter_xml = """<?xml version="1.0" encoding="UTF-8"?>
+            <wfs:GetFeature service="WFS" version="2.0.0" count="{{ count|e }}"
+                xmlns:wfs="http://www.opengis.net/wfs/2.0"
+                xmlns:fes="http://www.opengis.net/fes/2.0"
+                xmlns:gml="http://www.opengis.net/gml/3.2"
+                xmlns:sf="http://www.openplans.org/spearfish"
+                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+                xsi:schemaLocation="http://www.opengis.net/wfs/2.0
+                                    http://schemas.opengis.net/wfs/2.0/wfs.xsd
+                                    http://www.opengis.net/gml/3.2
+                                    http://schemas.opengis.net/gml/3.2.1/gml.xsd">
+                <wfs:Query typeNames="{{ workspace|e }}:{{ prefix|e }}_feature">
+                  <fes:Filter>
+                     <fes:And>
+                  
+                        <fes:BBOX>
+                           <fes:ValueReference>geom</fes:ValueReference>
+                           <gml:Envelope srsName="http://www.opengis.net/def/crs/epsg/0/4326">
+                              <gml:lowerCorner>{{ lowerCornerLat|e }} {{ lowerCornerLon|e }}</gml:lowerCorner>
+                              <gml:upperCorner>{{ upperCornerLat|e }} {{ upperCornerLon|e }}</gml:upperCorner>
+                           </gml:Envelope>
+                        </fes:BBOX>
+                            
+                        {% if begin_date %}
+                           <fes:PropertyIsGreaterThanOrEqualTo>
+                                <fes:ValueReference>max_timerecord</fes:ValueReference>
+                                <fes:Function name="dateParse">
+                                    <fes:Literal>yyyy-MM-dd</fes:Literal>
+                                    <fes:Literal>{{ begin_date|e }}</fes:Literal>
+                                </fes:Function>
+                           </fes:PropertyIsGreaterThanOrEqualTo>
+                        {% endif %}
+    
+                        {% if end_date %}
+                           <fes:PropertyIsLessThanOrEqualTo>
+                                <fes:ValueReference>min_timerecord</fes:ValueReference>
+                                <fes:Function name="dateParse">
+                                    <fes:Literal>yyyy-MM-dd</fes:Literal>
+                                    <fes:Literal>{{ end_date|e }}</fes:Literal>
+                                </fes:Function>
+                           </fes:PropertyIsLessThanOrEqualTo>
+                        {% endif %}
+                     </fes:And>
+                  </fes:Filter>
+                </wfs:Query>
+            </wfs:GetFeature>"""
+
+    template = Template(filter_xml)
+
+    return template.render(
+        workspace=workspace,
+        prefix=prefix,
+        count=count if count else 1000,
+        lowerCornerLon=float(coords[0]),
+        lowerCornerLat=float(coords[1]),
+        upperCornerLon=float(coords[2]),
+        upperCornerLat=float(coords[3]),
+        property_id=property_id,
+        begin_date=begin_date,
+        end_date=end_date,
+        threshold_value=threshold_value
+    )
diff --git a/backend/requirements.txt b/backend/requirements.txt
index caaba7116506b13ff555e3f345aae60b369fa73c..56454f318caa1ad4c848220b1a7abd752daca8b6 100644
--- a/backend/requirements.txt
+++ b/backend/requirements.txt
@@ -16,7 +16,6 @@ xarray==2024.6.0
 rasterio==1.3.10
 django-tabbed-changeform-admin==0.1.7
 pytz==2024.1
-OWSLib==0.31.0
 netCDF4==1.7.1
 h5netcdf==1.3.0
 pyproj==3.6.1
diff --git a/bin/build_thredds_image.sh b/bin/build_thredds_image.sh
old mode 100644
new mode 100755
diff --git a/container/backend-prod/Dockerfile b/container/backend-prod/Dockerfile
index 8348a3e576a12e9f123ff6356b78fcf7933e4103..92d82362d987c9c7f796117fa97364f8104d9e6d 100644
--- a/container/backend-prod/Dockerfile
+++ b/container/backend-prod/Dockerfile
@@ -1,4 +1,4 @@
-FROM debian:bullseye-slim as base
+FROM debian:bullseye-slim AS base
 
 RUN apt-get -y update \
     && apt-get -y dist-upgrade \
diff --git a/container/geoserver/Dockerfile b/container/geoserver/Dockerfile
index 3deec8de650031db6e900689f71c12357baa5546..fb91876db58832b8e47d9894eb0613c06635103e 100644
--- a/container/geoserver/Dockerfile
+++ b/container/geoserver/Dockerfile
@@ -1,8 +1,5 @@
-ARG IMAGE_VERSION=9.0.85-jdk11-temurin-jammy
+FROM tomcat:9.0.98-jdk17-temurin-jammy@sha256:1f35364892e7d90cc6ccbfe23b03d84791fbaaf4197171ea3cdac6672983de2f
 
-ARG JAVA_HOME=/usr/local/openjdk-11
-
-FROM tomcat:$IMAGE_VERSION
 
 RUN set -eux; \
     apt-get update; \
@@ -10,8 +7,8 @@ RUN set -eux; \
     && rm -rf /var/lib/apt/lists/*
 
 
-ENV GEOSERVER_VERSION 2.25.2
-ENV GEOSERVER_VERSION_MM 2.25
+ENV GEOSERVER_VERSION 2.26.1
+ENV GEOSERVER_VERSION_MM 2.26
 ENV GEOSERVER_HOME $CATALINA_HOME/geoserver
 ENV GEOSERVER_DATA_DIR /opt/geoserver/data_dir
 
@@ -35,7 +32,8 @@ RUN for plugin in netcdf netcdf-out; do \
 
 # INSTALL Community-Plugins
 
-RUN for plugin in cov-json cog-s3; do \
+
+RUN for plugin in cov-json cog-s3 sec-oauth2-github; do \
     curl -OL https://build.geoserver.org/geoserver/${GEOSERVER_VERSION_MM}.x/community-latest/geoserver-${GEOSERVER_VERSION_MM}-SNAPSHOT-${plugin}-plugin.zip && \
     unzip -o -d ${GEOSERVER_HOME}/webapps/geoserver/WEB-INF/lib/ geoserver-${GEOSERVER_VERSION_MM}-SNAPSHOT-${plugin}-plugin.zip; \
     done;
@@ -46,6 +44,9 @@ RUN cd ${GEOSERVER_HOME}/data_dir/ && \
 
 COPY usergroup ${GEOSERVER_HOME}/data_dir/security/usergroup/
 
+COPY web.xml ${GEOSERVER_HOME}/webapps/geoserver/WEB-INF/web.xml
+COPY server.xml /usr/local/tomcat/conf/server.xml
+
 COPY entrypoint.sh /usr/local/bin/
 
 ## redirect tomcat-base-path to GeoServer-Landingpage
diff --git a/container/geoserver/server.xml b/container/geoserver/server.xml
new file mode 100644
index 0000000000000000000000000000000000000000..eb3f6a70d88f27989a0f4025625208a275dacc48
--- /dev/null
+++ b/container/geoserver/server.xml
@@ -0,0 +1,183 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Licensed to the Apache Software Foundation (ASF) under one or more
+  contributor license agreements.  See the NOTICE file distributed with
+  this work for additional information regarding copyright ownership.
+  The ASF licenses this file to You under the Apache License, Version 2.0
+  (the "License"); you may not use this file except in compliance with
+  the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+  Unless required by applicable law or agreed to in writing, software
+  distributed under the License is distributed on an "AS IS" BASIS,
+  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+  See the License for the specific language governing permissions and
+  limitations under the License.
+-->
+<!-- Note:  A "Server" is not itself a "Container", so you may not
+     define subcomponents such as "Valves" at this level.
+     Documentation at /docs/config/server.html
+ -->
+<Server port="8005" shutdown="SHUTDOWN">
+  <Listener className="org.apache.catalina.startup.VersionLoggerListener" />
+  <!-- Security listener. Documentation at /docs/config/listeners.html
+  <Listener className="org.apache.catalina.security.SecurityListener" />
+  -->
+  <!-- APR library loader. Documentation at /docs/apr.html -->
+  <Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="on" />
+  <!-- Prevent memory leaks due to use of particular java/javax APIs-->
+  <Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
+  <Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
+  <Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
+
+  <!-- Global JNDI resources
+       Documentation at /docs/jndi-resources-howto.html
+  -->
+  <GlobalNamingResources>
+    <!-- Editable user database that can also be used by
+         UserDatabaseRealm to authenticate users
+    -->
+    <Resource name="UserDatabase" auth="Container"
+              type="org.apache.catalina.UserDatabase"
+              description="User database that can be updated and saved"
+              factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
+              pathname="conf/tomcat-users.xml" />
+  </GlobalNamingResources>
+
+  <!-- A "Service" is a collection of one or more "Connectors" that share
+       a single "Container" Note:  A "Service" is not itself a "Container",
+       so you may not define subcomponents such as "Valves" at this level.
+       Documentation at /docs/config/service.html
+   -->
+  <Service name="Catalina">
+
+    <!--The connectors can use a shared executor, you can define one or more named thread pools-->
+    <!--
+    <Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
+        maxThreads="150" minSpareThreads="4"/>
+    -->
+
+
+    <!-- A "Connector" represents an endpoint by which requests are received
+         and responses are returned. Documentation at :
+         Java HTTP Connector: /docs/config/http.html
+         Java AJP  Connector: /docs/config/ajp.html
+         APR (HTTP/AJP) Connector: /docs/apr.html
+         Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
+    -->
+    <Connector server="Apache" secure="true" port="8080" protocol="HTTP/1.1"
+        connectionTimeout="20000"
+        redirectPort="8443"
+        proxyName="web.app.ufz.de"
+        proxyPort="443"
+        scheme="https"
+    />
+
+    <!-- A "Connector" using the shared thread pool-->
+    <!--
+    <Connector executor="tomcatThreadPool"
+               port="8080" protocol="HTTP/1.1"
+               connectionTimeout="20000"
+               redirectPort="8443"
+               maxParameterCount="1000"
+               />
+    -->
+    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443
+         This connector uses the NIO implementation. The default
+         SSLImplementation will depend on the presence of the APR/native
+         library and the useOpenSSL attribute of the AprLifecycleListener.
+         Either JSSE or OpenSSL style configuration may be used regardless of
+         the SSLImplementation selected. JSSE style configuration is used below.
+    -->
+    <!--
+    <Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
+               maxThreads="150" SSLEnabled="true"
+               maxParameterCount="1000"
+               >
+        <SSLHostConfig>
+            <Certificate certificateKeystoreFile="conf/localhost-rsa.jks"
+                         type="RSA" />
+        </SSLHostConfig>
+    </Connector>
+    -->
+    <!-- Define an SSL/TLS HTTP/1.1 Connector on port 8443 with HTTP/2
+         This connector uses the APR/native implementation which always uses
+         OpenSSL for TLS.
+         Either JSSE or OpenSSL style configuration may be used. OpenSSL style
+         configuration is used below.
+    -->
+    <!--
+    <Connector port="8443" protocol="org.apache.coyote.http11.Http11AprProtocol"
+               maxThreads="150" SSLEnabled="true"
+               maxParameterCount="1000"
+               >
+        <UpgradeProtocol className="org.apache.coyote.http2.Http2Protocol" />
+        <SSLHostConfig>
+            <Certificate certificateKeyFile="conf/localhost-rsa-key.pem"
+                         certificateFile="conf/localhost-rsa-cert.pem"
+                         certificateChainFile="conf/localhost-rsa-chain.pem"
+                         type="RSA" />
+        </SSLHostConfig>
+    </Connector>
+    -->
+
+    <!-- Define an AJP 1.3 Connector on port 8009 -->
+    <!--
+    <Connector protocol="AJP/1.3"
+               address="::1"
+               port="8009"
+               redirectPort="8443"
+               maxParameterCount="1000"
+               />
+    -->
+
+    <!-- An Engine represents the entry point (within Catalina) that processes
+         every request.  The Engine implementation for Tomcat stand alone
+         analyzes the HTTP headers included with the request, and passes them
+         on to the appropriate Host (virtual host).
+         Documentation at /docs/config/engine.html -->
+
+    <!-- You should set jvmRoute to support load-balancing via AJP ie :
+    <Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
+    -->
+    <Engine name="Catalina" defaultHost="localhost">
+
+      <!--For clustering, please take a look at documentation at:
+          /docs/cluster-howto.html  (simple how to)
+          /docs/config/cluster.html (reference documentation) -->
+      <!--
+      <Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
+      -->
+
+      <!-- Use the LockOutRealm to prevent attempts to guess user passwords
+           via a brute-force attack -->
+      <Realm className="org.apache.catalina.realm.LockOutRealm">
+        <!-- This Realm uses the UserDatabase configured in the global JNDI
+             resources under the key "UserDatabase".  Any edits
+             that are performed against this UserDatabase are immediately
+             available for use by the Realm.  -->
+        <Realm className="org.apache.catalina.realm.UserDatabaseRealm"
+               resourceName="UserDatabase"/>
+      </Realm>
+
+      <Host name="localhost"  appBase="webapps"
+            unpackWARs="true" autoDeploy="true">
+
+        <!-- SingleSignOn valve, share authentication between web applications
+             Documentation at: /docs/config/valve.html -->
+        <!--
+        <Valve className="org.apache.catalina.authenticator.SingleSignOn" />
+        -->
+
+        <!-- Access log processes all example.
+             Documentation at: /docs/config/valve.html
+             Note: The pattern used is equivalent to using pattern="common" -->
+        <Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
+               prefix="localhost_access_log" suffix=".txt"
+               pattern="%h %l %u %t &quot;%r&quot; %s %b" />
+
+      </Host>
+    </Engine>
+  </Service>
+</Server>
diff --git a/container/geoserver/web.xml b/container/geoserver/web.xml
new file mode 100644
index 0000000000000000000000000000000000000000..aa0e4ff6fc78a543bcc9ffa8e56f69735829d8d5
--- /dev/null
+++ b/container/geoserver/web.xml
@@ -0,0 +1,297 @@
+<web-app>
+    <display-name>GeoServer</display-name>
+
+      <context-param>
+    <param-name>serviceStrategy</param-name>
+    <!-- Meaning of the different values :
+
+         PARTIAL-BUFFER2
+         - Partially buffers the first xKb to disk. Once that has buffered, the the
+           result is streamed to the user. This will allow for most errors to be caught
+           early.
+
+         BUFFER
+         - stores the entire response in memory first, before sending it off to
+           the user (may run out of memory)
+
+         SPEED
+         - outputs directly to the response (and cannot recover in the case of an
+           error)
+
+         FILE
+         - outputs to the local filesystem first, before sending it off to the user
+      -->
+    <param-value>PARTIAL-BUFFER2</param-value>
+  </context-param>
+
+  <context-param>
+    <!-- see comments on the PARTIAL-BUFFER strategy -->
+    <!-- this sets the size of the buffer.  default is "50" = 50kb -->
+
+    <param-name>PARTIAL_BUFFER_STRATEGY_SIZE</param-name>
+    <param-value>50</param-value>
+  </context-param>
+
+  <!--Can be true or false (defaults to: false). -->
+  <!--When true the JSONP (text/javascript) output format is enabled -->
+  <!--
+  <context-param>
+    <param-name>ENABLE_JSONP</param-name>
+    <param-value>true</param-value>
+  </context-param>
+  -->
+    <!-- -->
+    <context-param>
+      <param-name>PROXY_BASE_URL</param-name>
+      <param-value>https://web.app.ufz.de/geoserver</param-value>
+    </context-param>
+
+    <context-param>
+      <param-name>GEOSERVER_CSRF_WHITELIST</param-name>
+      <param-value>web.app.ufz.de</param-value>
+    </context-param>
+
+     <!--
+    <context-param>
+       <param-name>GEOSERVER_DATA_DIR</param-name>
+        <param-value>C:\eclipse\workspace\geoserver_trunk\cite\confCiteWFSPostGIS</param-value>
+    </context-param>
+   -->
+
+    <!-- pick up all spring application contexts -->
+    <context-param>
+        <param-name>contextConfigLocation</param-name>
+        <param-value>classpath*:/applicationContext.xml classpath*:/applicationSecurityContext.xml</param-value>
+    </context-param>
+
+    <filter>
+     <filter-name>FlushSafeFilter</filter-name>
+     <filter-class>org.geoserver.filters.FlushSafeFilter</filter-class>
+    </filter>
+
+    <filter>
+      <filter-name>Set Character Encoding</filter-name>
+      <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
+      <init-param>
+        <param-name>encoding</param-name>
+        <param-value>UTF-8</param-value>
+      </init-param>
+    </filter>
+
+    <filter>
+     <filter-name>SessionDebugger</filter-name>
+     <filter-class>org.geoserver.filters.SessionDebugFilter</filter-class>
+    </filter>
+
+    <filter>
+    <filter-name>filterChainProxy</filter-name>
+     <filter-class> org.springframework.web.filter.DelegatingFilterProxy</filter-class>
+    </filter>
+
+    <filter>
+      <filter-name>xFrameOptionsFilter</filter-name>
+      <filter-class>org.geoserver.filters.XFrameOptionsFilter</filter-class>
+    </filter>
+
+   <filter>
+     <filter-name>GZIP Compression Filter</filter-name>
+     <filter-class>org.geoserver.filters.GZIPFilter</filter-class>
+     <init-param>
+         <!-- The compressed-types parameter is a comma-separated list of regular expressions.
+              If a mime type matches any of the regular expressions then it will be compressed.
+              -->
+         <param-name>compressed-types</param-name>
+         <param-value>text/.*,.*xml.*,application/json,application/x-javascript</param-value>
+     </init-param>
+   </filter>
+
+   <filter>
+     <filter-name>Advanced Dispatch Filter</filter-name>
+     <filter-class>org.geoserver.platform.AdvancedDispatchFilter</filter-class>
+     <!--
+     This filter allows for a single mapping to the spring dispatcher. However using /* as a mapping
+     in a servlet mapping causes the servlet path to be "/" of the request. This causes problems with
+     library like wicket and restlet. So this filter fakes the servlet path by assuming the first
+     component of the path is the mapped path.
+     -->
+   </filter>
+
+   <filter>
+    <filter-name>Spring Delegating Filter</filter-name>
+    <filter-class>org.geoserver.filters.SpringDelegatingFilter</filter-class>
+    <!--
+    This filter allows for filters to be loaded via spring rather than
+    registered here in web.xml.  One thing to note is that for such filters
+    init() is not called. INstead any initialization is performed via spring
+    ioc.
+    -->
+   </filter>
+
+   <filter>
+     <filter-name>Thread locals cleanup filter</filter-name>
+     <filter-class>org.geoserver.filters.ThreadLocalsCleanupFilter</filter-class>
+     <!--
+     This filter cleans up thread locals Geotools is setting up for concurrency and performance
+     reasons
+     -->
+   </filter>
+
+   <!-- Uncomment following filter to enable CORS in Jetty. Do not forget the second config block further down.
+    -->
+
+    <filter>
+      <filter-name>cross-origin</filter-name>
+      <filter-class>org.eclipse.jetty.servlets.CrossOriginFilter</filter-class>
+      <init-param>
+        <param-name>chainPreflight</param-name>
+        <param-value>false</param-value>
+      </init-param>
+      <init-param>
+        <param-name>allowedOrigins</param-name>
+        <param-value>*</param-value>
+      </init-param>
+      <init-param>
+        <param-name>allowedMethods</param-name>
+        <param-value>GET,POST,PUT,DELETE,HEAD,OPTIONS</param-value>
+      </init-param>
+      <init-param>
+        <param-name>allowedHeaders</param-name>
+        <param-value>*</param-value>
+      </init-param>
+    </filter>
+
+   <!-- Uncomment following filter to enable CORS in Tomcat. Do not forget the second config block further down.
+    <filter>
+      <filter-name>cross-origin</filter-name>
+      <filter-class>org.apache.catalina.filters.CorsFilter</filter-class>
+      <init-param>
+        <param-name>cors.allowed.origins</param-name>
+        <param-value>*</param-value>
+      </init-param>
+      <init-param>
+        <param-name>cors.allowed.methods</param-name>
+        <param-value>GET,POST,PUT,DELETE,HEAD,OPTIONS</param-value>
+      </init-param>
+      <init-param>
+        <param-name>cors.allowed.headers</param-name>
+        <param-value>*</param-value>
+      </init-param>
+    </filter>
+   -->
+
+    <!--
+      THIS FILTER MAPPING MUST BE THE FIRST ONE, otherwise we end up with ruined chars in the input from the GUI
+      See the "Note" in the Tomcat character encoding guide:
+      http://wiki.apache.org/tomcat/FAQ/CharacterEncoding
+    -->
+    <filter-mapping>
+      <filter-name>Set Character Encoding</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+   <!-- Uncomment following filter to enable CORS
+   -->
+    <filter-mapping>
+        <filter-name>cross-origin</filter-name>
+        <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>FlushSafeFilter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>SessionDebugger</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>GZIP Compression Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>xFrameOptionsFilter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <!--
+      If you want to use your security system comment out this one too
+    -->
+    <filter-mapping>
+      <filter-name>filterChainProxy</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>Advanced Dispatch Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>Spring Delegating Filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <filter-mapping>
+      <filter-name>Thread locals cleanup filter</filter-name>
+      <url-pattern>/*</url-pattern>
+    </filter-mapping>
+
+    <!-- general initializer, should be first thing to execute -->
+    <listener>
+      <listener-class>org.geoserver.GeoserverInitStartupListener</listener-class>
+    </listener>
+
+    <!-- logging initializer, should execute before spring context startup -->
+    <listener>
+      <listener-class>org.geoserver.logging.LoggingStartupContextListener</listener-class>
+    </listener>
+
+    <!--  spring context loader -->
+    <listener>
+      <listener-class>org.geoserver.platform.GeoServerContextLoaderListener</listener-class>
+    </listener>
+
+    <!--  http session listener proxy -->
+    <listener>
+      <listener-class>org.geoserver.platform.GeoServerHttpSessionListenerProxy</listener-class>
+    </listener>
+
+        <!-- request context listener for session-scoped beans -->
+        <listener>
+                <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
+        </listener>
+
+    <!-- spring dispatcher servlet, dispatches all incoming requests -->
+    <servlet>
+      <servlet-name>dispatcher</servlet-name>
+      <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
+    </servlet>
+
+    <!-- single mapping to spring, this only works properly if the advanced dispatch filter is
+         active -->
+    <servlet-mapping>
+        <servlet-name>dispatcher</servlet-name>
+        <url-pattern>/*</url-pattern>
+    </servlet-mapping>
+
+    <mime-mapping>
+      <extension>xsl</extension>
+      <mime-type>text/xml</mime-type>
+    </mime-mapping>
+    <mime-mapping>
+      <extension>sld</extension>
+      <mime-type>text/xml</mime-type>
+    </mime-mapping>
+    <mime-mapping>
+      <extension>json</extension>
+      <mime-type>application/json</mime-type>
+    </mime-mapping>
+
+    <welcome-file-list>
+        <welcome-file>index.html</welcome-file>
+    </welcome-file-list>
+
+</web-app>
diff --git a/container/postgres/create-databases.sh b/container/postgres/create-databases.sh
index ba33d9ce36aad11e55768650b1b0a0a8a14af739..c1bef18a8d472647bde50e0e0780dc6f64e55c90 100644
--- a/container/postgres/create-databases.sh
+++ b/container/postgres/create-databases.sh
@@ -5,21 +5,36 @@ set -u
 
 function create_user_and_database() {
 	local database=$1
+
 	echo "  Creating user and database '$database'"
+
 	psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
 	    CREATE USER $database;
 	    CREATE DATABASE $database;
 	    GRANT ALL PRIVILEGES ON DATABASE $database TO $database;
 EOSQL
-  psql -d $database -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
-	    CREATE EXTENSION postgis;
-EOSQL
 
 }
 
-if [ -n "$POSTGRES_MULTIPLE_DATABASES" ]; then
-	for db in $(echo $POSTGRES_MULTIPLE_DATABASES | tr ',' ' '); do
-		create_user_and_database $db
-	done
-	echo "Databases created"
-fi
\ No newline at end of file
+declare -a databases=("$DJANGO_DATABASE" "$FROST_DATABASE" "$GEONETWORK_DB")
+
+for db in "${databases[@]}"; do
+  create_user_and_database $db
+done
+echo "Databases created"
+
+
+psql -d $DJANGO_DATABASE -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
+    CREATE SCHEMA geoserver;
+
+    CREATE EXTENSION postgis SCHEMA geoserver;
+
+    ALTER DATABASE django SET search_path = public,geoserver,postgis;
+EOSQL
+
+
+psql -d $FROST_DATABASE -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
+    CREATE EXTENSION postgis;
+EOSQL
+
+echo "PostGIS installed"
diff --git a/container/thredds/Dockerfile b/container/thredds/Dockerfile
index e2232c996d4da62c9c69adf32e197d3ff11178a7..4d6444c843c3a0b28ba2eff3af238ef39b83ef1c 100644
--- a/container/thredds/Dockerfile
+++ b/container/thredds/Dockerfile
@@ -1,4 +1,4 @@
-FROM unidata/thredds-docker:5.4
+FROM unidata/thredds-docker:5.5
 
 COPY threddsConfig.xml ${CATALINA_HOME}/content/thredds/threddsConfig.xml
 COPY tomcat-users.xml ${CATALINA_HOME}/conf/tomcat-users.xml
diff --git a/container/thredds/server.xml b/container/thredds/server.xml
index 058bd2ec82a251fc318c5c758c4f1315649ca8f0..c514fc3af8f344321dc3aa412c57f12b113f17cb 100644
--- a/container/thredds/server.xml
+++ b/container/thredds/server.xml
@@ -69,7 +69,7 @@
     <Connector server="Apache" secure="true" port="8080" protocol="HTTP/1.1"
                connectionTimeout="20000"
                redirectPort="8443"
-               proxyName="gdi-fs.ufz.de"
+               proxyName="web.app.ufz.de"
                proxyPort="443"
                scheme="https"
     />
diff --git a/container/thredds/threddsConfig.xml b/container/thredds/threddsConfig.xml
index 88fb118076b3df16586d98f64661717dfff88012..83b47e20c12d4ad2246e53cbd3c375076f7dd413 100644
--- a/container/thredds/threddsConfig.xml
+++ b/container/thredds/threddsConfig.xml
@@ -12,9 +12,9 @@
     <keywords>meteorology, atmosphere, climate, ocean, earth science</keywords>
 
     <contact>
-      <name>IT-Support</name>
+      <name>SDI-Team</name>
       <organization>Research Data Management (RDM)</organization>
-      <email>rdm-contact@ufz.de</email>
+      <email>sdi@ufz.de</email>
       <!--phone></phone-->
     </contact>
     <hostInstitution>
diff --git a/docker-compose-prod.yml b/docker-compose-prod.yml
index 7563dce01f193493662d260b95631ae9a329fc50..e83e3bb7520d44a56755781e7b9928f49c081e4e 100644
--- a/docker-compose-prod.yml
+++ b/docker-compose-prod.yml
@@ -1,5 +1,5 @@
 x-image2: &backend-image
-  image: ${REPOSITORY}backend:3.14.1
+  image: ${REPOSITORY}backend:3.14.2
   build:
     context: .
     dockerfile: ./container/backend-prod/Dockerfile
@@ -41,7 +41,7 @@ services:
       - DJANGO_SECRET_KEY=${DJANGO_SECRET_KEY}
 
   pygeoapi:
-    image: git.ufz.de:4567/rdm/wt-systeme/ufz-gdi/pygeoapi:1.0.0
+    image: ${REPOSITORY}pygeoapi:1.0.0
     build:
       context: .
       dockerfile: ./container/pygeoapi/Dockerfile-for-production
diff --git a/docker-compose.yml b/docker-compose.yml
index 4a4f37dbcb09c048c1c367759f3e5ede10f855d5..ffcb4bcba86edd96828c61cc91ea6540bbb3de51 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -17,7 +17,7 @@ services:
     entrypoint: ["sh", "/etc/entrypoint.sh"]
 
   geoserver:
-    image: ${REPOSITORY}geoserver:2.25.2
+    image: ${REPOSITORY}tanzu-geoserver:2.26.1b
     build:
       context: ./container/geoserver
       dockerfile: ./Dockerfile
@@ -104,7 +104,9 @@ services:
     environment:
       - POSTGRES_USER=${POSTGRES_USERNAME}
       - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
-      - POSTGRES_MULTIPLE_DATABASES=${DJANGO_POSTGRES_DB},${FROST_POSTGRES_DB},${GEONETWORK_POSTGRES_DB}
+      - DJANGO_DATABASE=${DJANGO_POSTGRES_DB}
+      - FROST_DATABASE=${FROST_POSTGRES_DB}
+      - GEONETWORK_DB=${GEONETWORK_POSTGRES_DB}
     healthcheck:
       test: ["CMD-SHELL", "pg_isready -U $POSTGRES_USERNAME"]
       interval: 40s
@@ -152,7 +154,7 @@ services:
         condition: service_healthy
 
   thredds:
-    image: ${REPOSITORY}thredds:5.4.0
+    image: ${REPOSITORY}thredds:5.5.0
     build:
       context: ./container/thredds
       dockerfile: ./Dockerfile
diff --git a/frontend/index.html b/frontend/index.html
index 8398773fc70d76fffa6e051f3b7e5241f045a255..3e1fdce4a4b9441331051c89e5b0950f334272a4 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -6,7 +6,6 @@
     <meta name="viewport" content="width=device-width, initial-scale=1">
     <meta name="robots" content="noindex" />
     <link rel="icon" href="/favicon.ico">
-    <link rel="stylesheet" href="/blue_theme.css">
   </head>
   <body>
     <noscript>
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index b8ded0a05ba9e8708bba2d58d049075989da1cdf..93c286cf3e29351f5c11f42c79802b9019914bd1 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -21,6 +21,7 @@
         "html2canvas": "^1.4.1",
         "jspdf": "^2.5.1",
         "marked": "^13.0.0",
+        "pinia": "^2.2.6",
         "plotly.js-basic-dist-min": "^2.33.0",
         "plotly.js-locales": "^2.33.0",
         "proj4": "^2.11.0",
@@ -48,11 +49,32 @@
         "vue-tsc": "^2.1.0"
       }
     },
+    "node_modules/@babel/helper-string-parser": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+      "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
+    "node_modules/@babel/helper-validator-identifier": {
+      "version": "7.25.9",
+      "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+      "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+      "license": "MIT",
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@babel/parser": {
-      "version": "7.24.7",
-      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.7.tgz",
-      "integrity": "sha512-9uUYRm6OqQrCqQdG1iCBwBPZgN8ciDBro2nIOFaiRz1/BCxaI7CNvQbDHvsArAC7Tw9Hda/B3U+6ui9u4HWXPw==",
+      "version": "7.26.2",
+      "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz",
+      "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==",
       "license": "MIT",
+      "dependencies": {
+        "@babel/types": "^7.26.0"
+      },
       "bin": {
         "parser": "bin/babel-parser.js"
       },
@@ -72,6 +94,19 @@
         "node": ">=6.9.0"
       }
     },
+    "node_modules/@babel/types": {
+      "version": "7.26.0",
+      "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz",
+      "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==",
+      "license": "MIT",
+      "dependencies": {
+        "@babel/helper-string-parser": "^7.25.9",
+        "@babel/helper-validator-identifier": "^7.25.9"
+      },
+      "engines": {
+        "node": ">=6.9.0"
+      }
+    },
     "node_modules/@esbuild/aix-ppc64": {
       "version": "0.21.5",
       "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
@@ -646,13 +681,13 @@
       "peer": true
     },
     "node_modules/@intlify/core-base": {
-      "version": "9.13.1",
-      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.13.1.tgz",
-      "integrity": "sha512-+bcQRkJO9pcX8d0gel9ZNfrzU22sZFSA0WVhfXrf5jdJOS24a+Bp8pozuS9sBI9Hk/tGz83pgKfmqcn/Ci7/8w==",
+      "version": "9.14.2",
+      "resolved": "https://registry.npmjs.org/@intlify/core-base/-/core-base-9.14.2.tgz",
+      "integrity": "sha512-DZyQ4Hk22sC81MP4qiCDuU+LdaYW91A6lCjq8AWPvY3+mGMzhGDfOCzvyR6YBQxtlPjFqMoFk9ylnNYRAQwXtQ==",
       "license": "MIT",
       "dependencies": {
-        "@intlify/message-compiler": "9.13.1",
-        "@intlify/shared": "9.13.1"
+        "@intlify/message-compiler": "9.14.2",
+        "@intlify/shared": "9.14.2"
       },
       "engines": {
         "node": ">= 16"
@@ -662,12 +697,12 @@
       }
     },
     "node_modules/@intlify/message-compiler": {
-      "version": "9.13.1",
-      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.13.1.tgz",
-      "integrity": "sha512-SKsVa4ajYGBVm7sHMXd5qX70O2XXjm55zdZB3VeMFCvQyvLew/dLvq3MqnaIsTMF1VkkOb9Ttr6tHcMlyPDL9w==",
+      "version": "9.14.2",
+      "resolved": "https://registry.npmjs.org/@intlify/message-compiler/-/message-compiler-9.14.2.tgz",
+      "integrity": "sha512-YsKKuV4Qv4wrLNsvgWbTf0E40uRv+Qiw1BeLQ0LAxifQuhiMe+hfTIzOMdWj/ZpnTDj4RSZtkXjJM7JDiiB5LQ==",
       "license": "MIT",
       "dependencies": {
-        "@intlify/shared": "9.13.1",
+        "@intlify/shared": "9.14.2",
         "source-map-js": "^1.0.2"
       },
       "engines": {
@@ -678,9 +713,9 @@
       }
     },
     "node_modules/@intlify/shared": {
-      "version": "9.13.1",
-      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.13.1.tgz",
-      "integrity": "sha512-u3b6BKGhE6j/JeRU6C/RL2FgyJfy6LakbtfeVF8fJXURpZZTzfh3e05J0bu0XPw447Q6/WUp3C4ajv4TMS4YsQ==",
+      "version": "9.14.2",
+      "resolved": "https://registry.npmjs.org/@intlify/shared/-/shared-9.14.2.tgz",
+      "integrity": "sha512-uRAHAxYPeF+G5DBIboKpPgC/Waecd4Jz8ihtkpJQD5ycb5PwXp0k/+hBGl5dAjwF7w+l74kz/PKA8r8OK//RUw==",
       "license": "MIT",
       "engines": {
         "node": ">= 16"
@@ -737,9 +772,9 @@
       }
     },
     "node_modules/@jridgewell/sourcemap-codec": {
-      "version": "1.4.15",
-      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz",
-      "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==",
+      "version": "1.5.0",
+      "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+      "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
       "license": "MIT"
     },
     "node_modules/@nodelib/fs.scandir": {
@@ -849,9 +884,9 @@
       }
     },
     "node_modules/@rollup/rollup-android-arm-eabi": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.18.0.tgz",
-      "integrity": "sha512-Tya6xypR10giZV1XzxmH5wr25VcZSncG0pZIjfePT0OVBvqNEurzValetGNarVrGiq66EBVAFn15iYX4w6FKgQ==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.27.4.tgz",
+      "integrity": "sha512-2Y3JT6f5MrQkICUyRVCw4oa0sutfAsgaSsb0Lmmy1Wi2y7X5vT9Euqw4gOsCyy0YfKURBg35nhUKZS4mDcfULw==",
       "cpu": [
         "arm"
       ],
@@ -862,9 +897,9 @@
       ]
     },
     "node_modules/@rollup/rollup-android-arm64": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.18.0.tgz",
-      "integrity": "sha512-avCea0RAP03lTsDhEyfy+hpfr85KfyTctMADqHVhLAF3MlIkq83CP8UfAHUssgXTYd+6er6PaAhx/QGv4L1EiA==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.27.4.tgz",
+      "integrity": "sha512-wzKRQXISyi9UdCVRqEd0H4cMpzvHYt1f/C3CoIjES6cG++RHKhrBj2+29nPF0IB5kpy9MS71vs07fvrNGAl/iA==",
       "cpu": [
         "arm64"
       ],
@@ -875,9 +910,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-arm64": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.18.0.tgz",
-      "integrity": "sha512-IWfdwU7KDSm07Ty0PuA/W2JYoZ4iTj3TUQjkVsO/6U+4I1jN5lcR71ZEvRh52sDOERdnNhhHU57UITXz5jC1/w==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.27.4.tgz",
+      "integrity": "sha512-PlNiRQapift4LNS8DPUHuDX/IdXiLjf8mc5vdEmUR0fF/pyy2qWwzdLjB+iZquGr8LuN4LnUoSEvKRwjSVYz3Q==",
       "cpu": [
         "arm64"
       ],
@@ -888,9 +923,9 @@
       ]
     },
     "node_modules/@rollup/rollup-darwin-x64": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.18.0.tgz",
-      "integrity": "sha512-n2LMsUz7Ynu7DoQrSQkBf8iNrjOGyPLrdSg802vk6XT3FtsgX6JbE8IHRvposskFm9SNxzkLYGSq9QdpLYpRNA==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.27.4.tgz",
+      "integrity": "sha512-o9bH2dbdgBDJaXWJCDTNDYa171ACUdzpxSZt+u/AAeQ20Nk5x+IhA+zsGmrQtpkLiumRJEYef68gcpn2ooXhSQ==",
       "cpu": [
         "x64"
       ],
@@ -900,10 +935,36 @@
         "darwin"
       ]
     },
+    "node_modules/@rollup/rollup-freebsd-arm64": {
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.27.4.tgz",
+      "integrity": "sha512-NBI2/i2hT9Q+HySSHTBh52da7isru4aAAo6qC3I7QFVsuhxi2gM8t/EI9EVcILiHLj1vfi+VGGPaLOUENn7pmw==",
+      "cpu": [
+        "arm64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
+    "node_modules/@rollup/rollup-freebsd-x64": {
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.27.4.tgz",
+      "integrity": "sha512-wYcC5ycW2zvqtDYrE7deary2P2UFmSh85PUpAx+dwTCO9uw3sgzD6Gv9n5X4vLaQKsrfTSZZ7Z7uynQozPVvWA==",
+      "cpu": [
+        "x64"
+      ],
+      "license": "MIT",
+      "optional": true,
+      "os": [
+        "freebsd"
+      ]
+    },
     "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.18.0.tgz",
-      "integrity": "sha512-C/zbRYRXFjWvz9Z4haRxcTdnkPt1BtCkz+7RtBSuNmKzMzp3ZxdM28Mpccn6pt28/UWUCTXa+b0Mx1k3g6NOMA==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.27.4.tgz",
+      "integrity": "sha512-9OwUnK/xKw6DyRlgx8UizeqRFOfi9mf5TYCw1uolDaJSbUmBxP85DE6T4ouCMoN6pXw8ZoTeZCSEfSaYo+/s1w==",
       "cpu": [
         "arm"
       ],
@@ -914,9 +975,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm-musleabihf": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.18.0.tgz",
-      "integrity": "sha512-l3m9ewPgjQSXrUMHg93vt0hYCGnrMOcUpTz6FLtbwljo2HluS4zTXFy2571YQbisTnfTKPZ01u/ukJdQTLGh9A==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.27.4.tgz",
+      "integrity": "sha512-Vgdo4fpuphS9V24WOV+KwkCVJ72u7idTgQaBoLRD0UxBAWTF9GWurJO9YD9yh00BzbkhpeXtm6na+MvJU7Z73A==",
       "cpu": [
         "arm"
       ],
@@ -927,9 +988,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-gnu": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.18.0.tgz",
-      "integrity": "sha512-rJ5D47d8WD7J+7STKdCUAgmQk49xuFrRi9pZkWoRD1UeSMakbcepWXPF8ycChBoAqs1pb2wzvbY6Q33WmN2ftw==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.27.4.tgz",
+      "integrity": "sha512-pleyNgyd1kkBkw2kOqlBx+0atfIIkkExOTiifoODo6qKDSpnc6WzUY5RhHdmTdIJXBdSnh6JknnYTtmQyobrVg==",
       "cpu": [
         "arm64"
       ],
@@ -940,9 +1001,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-arm64-musl": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.18.0.tgz",
-      "integrity": "sha512-be6Yx37b24ZwxQ+wOQXXLZqpq4jTckJhtGlWGZs68TgdKXJgw54lUUoFYrg6Zs/kjzAQwEwYbp8JxZVzZLRepQ==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.27.4.tgz",
+      "integrity": "sha512-caluiUXvUuVyCHr5DxL8ohaaFFzPGmgmMvwmqAITMpV/Q+tPoaHZ/PWa3t8B2WyoRcIIuu1hkaW5KkeTDNSnMA==",
       "cpu": [
         "arm64"
       ],
@@ -953,9 +1014,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.18.0.tgz",
-      "integrity": "sha512-hNVMQK+qrA9Todu9+wqrXOHxFiD5YmdEi3paj6vP02Kx1hjd2LLYR2eaN7DsEshg09+9uzWi2W18MJDlG0cxJA==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.27.4.tgz",
+      "integrity": "sha512-FScrpHrO60hARyHh7s1zHE97u0KlT/RECzCKAdmI+LEoC1eDh/RDji9JgFqyO+wPDb86Oa/sXkily1+oi4FzJQ==",
       "cpu": [
         "ppc64"
       ],
@@ -966,9 +1027,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-riscv64-gnu": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.18.0.tgz",
-      "integrity": "sha512-ROCM7i+m1NfdrsmvwSzoxp9HFtmKGHEqu5NNDiZWQtXLA8S5HBCkVvKAxJ8U+CVctHwV2Gb5VUaK7UAkzhDjlg==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.27.4.tgz",
+      "integrity": "sha512-qyyprhyGb7+RBfMPeww9FlHwKkCXdKHeGgSqmIXw9VSUtvyFZ6WZRtnxgbuz76FK7LyoN8t/eINRbPUcvXB5fw==",
       "cpu": [
         "riscv64"
       ],
@@ -979,9 +1040,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-s390x-gnu": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.18.0.tgz",
-      "integrity": "sha512-0UyyRHyDN42QL+NbqevXIIUnKA47A+45WyasO+y2bGJ1mhQrfrtXUpTxCOrfxCR4esV3/RLYyucGVPiUsO8xjg==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.27.4.tgz",
+      "integrity": "sha512-PFz+y2kb6tbh7m3A7nA9++eInGcDVZUACulf/KzDtovvdTizHpZaJty7Gp0lFwSQcrnebHOqxF1MaKZd7psVRg==",
       "cpu": [
         "s390x"
       ],
@@ -992,9 +1053,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-gnu": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.18.0.tgz",
-      "integrity": "sha512-xuglR2rBVHA5UsI8h8UbX4VJ470PtGCf5Vpswh7p2ukaqBGFTnsfzxUBetoWBWymHMxbIG0Cmx7Y9qDZzr648w==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.27.4.tgz",
+      "integrity": "sha512-Ni8mMtfo+o/G7DVtweXXV/Ol2TFf63KYjTtoZ5f078AUgJTmaIJnj4JFU7TK/9SVWTaSJGxPi5zMDgK4w+Ez7Q==",
       "cpu": [
         "x64"
       ],
@@ -1005,9 +1066,9 @@
       ]
     },
     "node_modules/@rollup/rollup-linux-x64-musl": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.18.0.tgz",
-      "integrity": "sha512-LKaqQL9osY/ir2geuLVvRRs+utWUNilzdE90TpyoX0eNqPzWjRm14oMEE+YLve4k/NAqCdPkGYDaDF5Sw+xBfg==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.27.4.tgz",
+      "integrity": "sha512-5AeeAF1PB9TUzD+3cROzFTnAJAcVUGLuR8ng0E0WXGkYhp6RD6L+6szYVX+64Rs0r72019KHZS1ka1q+zU/wUw==",
       "cpu": [
         "x64"
       ],
@@ -1018,9 +1079,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-arm64-msvc": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.18.0.tgz",
-      "integrity": "sha512-7J6TkZQFGo9qBKH0pk2cEVSRhJbL6MtfWxth7Y5YmZs57Pi+4x6c2dStAUvaQkHQLnEQv1jzBUW43GvZW8OFqA==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.27.4.tgz",
+      "integrity": "sha512-yOpVsA4K5qVwu2CaS3hHxluWIK5HQTjNV4tWjQXluMiiiu4pJj4BN98CvxohNCpcjMeTXk/ZMJBRbgRg8HBB6A==",
       "cpu": [
         "arm64"
       ],
@@ -1031,9 +1092,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-ia32-msvc": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.18.0.tgz",
-      "integrity": "sha512-Txjh+IxBPbkUB9+SXZMpv+b/vnTEtFyfWZgJ6iyCmt2tdx0OF5WhFowLmnh8ENGNpfUlUZkdI//4IEmhwPieNg==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.27.4.tgz",
+      "integrity": "sha512-KtwEJOaHAVJlxV92rNYiG9JQwQAdhBlrjNRp7P9L8Cb4Rer3in+0A+IPhJC9y68WAi9H0sX4AiG2NTsVlmqJeQ==",
       "cpu": [
         "ia32"
       ],
@@ -1044,9 +1105,9 @@
       ]
     },
     "node_modules/@rollup/rollup-win32-x64-msvc": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.18.0.tgz",
-      "integrity": "sha512-UOo5FdvOL0+eIVTgS4tIdbW+TtnBLWg1YBCcU2KWM7nuNwRz9bksDX1bekJJCpu25N1DVWaCwnT39dVQxzqS8g==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.27.4.tgz",
+      "integrity": "sha512-3j4jx1TppORdTAoBJRd+/wJRGCPC0ETWkXOecJ6PPZLj6SptXkrXcNqdj0oclbKML6FkQltdz7bBA3rUSirZug==",
       "cpu": [
         "x64"
       ],
@@ -1057,9 +1118,9 @@
       ]
     },
     "node_modules/@types/estree": {
-      "version": "1.0.5",
-      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
-      "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+      "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
       "license": "MIT"
     },
     "node_modules/@types/geojson": {
@@ -1396,53 +1457,53 @@
       }
     },
     "node_modules/@vue/compiler-core": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.4.29.tgz",
-      "integrity": "sha512-TFKiRkKKsRCKvg/jTSSKK7mYLJEQdUiUfykbG49rubC9SfDyvT2JrzTReopWlz2MxqeLyxh9UZhvxEIBgAhtrg==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
+      "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
       "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.24.7",
-        "@vue/shared": "3.4.29",
+        "@babel/parser": "^7.25.3",
+        "@vue/shared": "3.5.13",
         "entities": "^4.5.0",
         "estree-walker": "^2.0.2",
         "source-map-js": "^1.2.0"
       }
     },
     "node_modules/@vue/compiler-dom": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.4.29.tgz",
-      "integrity": "sha512-A6+iZ2fKIEGnfPJejdB7b1FlJzgiD+Y/sxxKwJWg1EbJu6ZPgzaPQQ51ESGNv0CP6jm6Z7/pO6Ia8Ze6IKrX7w==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
+      "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
       "license": "MIT",
       "dependencies": {
-        "@vue/compiler-core": "3.4.29",
-        "@vue/shared": "3.4.29"
+        "@vue/compiler-core": "3.5.13",
+        "@vue/shared": "3.5.13"
       }
     },
     "node_modules/@vue/compiler-sfc": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.4.29.tgz",
-      "integrity": "sha512-zygDcEtn8ZimDlrEQyLUovoWgKQic6aEQqRXce2WXBvSeHbEbcAsXyCk9oG33ZkyWH4sl9D3tkYc1idoOkdqZQ==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
+      "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
       "license": "MIT",
       "dependencies": {
-        "@babel/parser": "^7.24.7",
-        "@vue/compiler-core": "3.4.29",
-        "@vue/compiler-dom": "3.4.29",
-        "@vue/compiler-ssr": "3.4.29",
-        "@vue/shared": "3.4.29",
+        "@babel/parser": "^7.25.3",
+        "@vue/compiler-core": "3.5.13",
+        "@vue/compiler-dom": "3.5.13",
+        "@vue/compiler-ssr": "3.5.13",
+        "@vue/shared": "3.5.13",
         "estree-walker": "^2.0.2",
-        "magic-string": "^0.30.10",
-        "postcss": "^8.4.38",
+        "magic-string": "^0.30.11",
+        "postcss": "^8.4.48",
         "source-map-js": "^1.2.0"
       }
     },
     "node_modules/@vue/compiler-ssr": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.4.29.tgz",
-      "integrity": "sha512-rFbwCmxJ16tDp3N8XCx5xSQzjhidYjXllvEcqX/lopkoznlNPz3jyy0WGJCyhAaVQK677WWFt3YO/WUEkMMUFQ==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
+      "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
       "license": "MIT",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.29",
-        "@vue/shared": "3.4.29"
+        "@vue/compiler-dom": "3.5.13",
+        "@vue/shared": "3.5.13"
       }
     },
     "node_modules/@vue/compiler-vue2": {
@@ -1554,53 +1615,53 @@
       }
     },
     "node_modules/@vue/reactivity": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.4.29.tgz",
-      "integrity": "sha512-w8+KV+mb1a8ornnGQitnMdLfE0kXmteaxLdccm2XwdFxXst4q/Z7SEboCV5SqJNpZbKFeaRBBJBhW24aJyGINg==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
+      "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
       "license": "MIT",
       "dependencies": {
-        "@vue/shared": "3.4.29"
+        "@vue/shared": "3.5.13"
       }
     },
     "node_modules/@vue/runtime-core": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.4.29.tgz",
-      "integrity": "sha512-s8fmX3YVR/Rk5ig0ic0NuzTNjK2M7iLuVSZyMmCzN/+Mjuqqif1JasCtEtmtoJWF32pAtUjyuT2ljNKNLeOmnQ==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
+      "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
       "license": "MIT",
       "dependencies": {
-        "@vue/reactivity": "3.4.29",
-        "@vue/shared": "3.4.29"
+        "@vue/reactivity": "3.5.13",
+        "@vue/shared": "3.5.13"
       }
     },
     "node_modules/@vue/runtime-dom": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.4.29.tgz",
-      "integrity": "sha512-gI10atCrtOLf/2MPPMM+dpz3NGulo9ZZR9d1dWo4fYvm+xkfvRrw1ZmJ7mkWtiJVXSsdmPbcK1p5dZzOCKDN0g==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
+      "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
       "license": "MIT",
       "dependencies": {
-        "@vue/reactivity": "3.4.29",
-        "@vue/runtime-core": "3.4.29",
-        "@vue/shared": "3.4.29",
+        "@vue/reactivity": "3.5.13",
+        "@vue/runtime-core": "3.5.13",
+        "@vue/shared": "3.5.13",
         "csstype": "^3.1.3"
       }
     },
     "node_modules/@vue/server-renderer": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.4.29.tgz",
-      "integrity": "sha512-HMLCmPI2j/k8PVkSBysrA2RxcxC5DgBiCdj7n7H2QtR8bQQPqKAe8qoaxLcInzouBmzwJ+J0x20ygN/B5mYBng==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
+      "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
       "license": "MIT",
       "dependencies": {
-        "@vue/compiler-ssr": "3.4.29",
-        "@vue/shared": "3.4.29"
+        "@vue/compiler-ssr": "3.5.13",
+        "@vue/shared": "3.5.13"
       },
       "peerDependencies": {
-        "vue": "3.4.29"
+        "vue": "3.5.13"
       }
     },
     "node_modules/@vue/shared": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.4.29.tgz",
-      "integrity": "sha512-hQ2gAQcBO/CDpC82DCrinJNgOHI2v+FA7BDW4lMSPeBpQ7sRe2OLHWe5cph1s7D8DUQAwRt18dBDfJJ220APEA==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
+      "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
       "license": "MIT"
     },
     "node_modules/@vue/test-utils": {
@@ -1773,9 +1834,9 @@
       }
     },
     "node_modules/axios": {
-      "version": "1.7.2",
-      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.2.tgz",
-      "integrity": "sha512-2A8QhOMrbomlDuiLeK9XibIBzuHeRcqqNOHp0Cyp5EoJ1IFDh+XZH3A6BkXtv0K4gFGCI0Y4BM7B1wOEi0Rmgw==",
+      "version": "1.7.8",
+      "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.8.tgz",
+      "integrity": "sha512-Uu0wb7KNqK2t5K+YQyVCLM76prD5sRFjKHbJYCP1J7JFGEQ6nN7HWn9+04LAeiJ3ji54lgS/gZCH1oxyrf1SPw==",
       "license": "MIT",
       "dependencies": {
         "follow-redirects": "^1.15.6",
@@ -2295,9 +2356,9 @@
       "license": "MIT"
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -2568,9 +2629,9 @@
       }
     },
     "node_modules/elliptic": {
-      "version": "6.5.5",
-      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.5.tgz",
-      "integrity": "sha512-7EjbcmUm17NQFu4Pmgmq2olYMj8nwMnpcddByChSUjArp8F5DQWcIcpriwO4ZToLNAJig0yiyjswfyGNje/ixw==",
+      "version": "6.6.1",
+      "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz",
+      "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==",
       "license": "MIT",
       "dependencies": {
         "bn.js": "^4.11.9",
@@ -3974,12 +4035,12 @@
       }
     },
     "node_modules/magic-string": {
-      "version": "0.30.10",
-      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.10.tgz",
-      "integrity": "sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==",
+      "version": "0.30.14",
+      "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.14.tgz",
+      "integrity": "sha512-5c99P1WKTed11ZC0HMJOj6CDIue6F8ySu+bJL+85q1zBEIY8IklrJ1eiKC2NDRh3Ct3FcvmJPyQHb9erXMTJNw==",
       "license": "MIT",
       "dependencies": {
-        "@jridgewell/sourcemap-codec": "^1.4.15"
+        "@jridgewell/sourcemap-codec": "^1.5.0"
       }
     },
     "node_modules/marked": {
@@ -4022,9 +4083,9 @@
       "license": "MIT"
     },
     "node_modules/micromatch": {
-      "version": "4.0.7",
-      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
-      "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
+      "version": "4.0.8",
+      "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+      "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -4126,9 +4187,9 @@
       "license": "MIT"
     },
     "node_modules/nanoid": {
-      "version": "3.3.7",
-      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
-      "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+      "version": "3.3.8",
+      "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+      "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
       "funding": [
         {
           "type": "github",
@@ -4593,9 +4654,9 @@
       "optional": true
     },
     "node_modules/picocolors": {
-      "version": "1.0.1",
-      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz",
-      "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==",
+      "version": "1.1.1",
+      "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+      "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
       "license": "ISC"
     },
     "node_modules/picomatch": {
@@ -4610,6 +4671,58 @@
         "url": "https://github.com/sponsors/jonschlinkert"
       }
     },
+    "node_modules/pinia": {
+      "version": "2.2.6",
+      "resolved": "https://registry.npmjs.org/pinia/-/pinia-2.2.6.tgz",
+      "integrity": "sha512-vIsR8JkDN5Ga2vAxqOE2cJj4VtsHnzpR1Fz30kClxlh0yCHfec6uoMeM3e/ddqmwFUejK3NlrcQa/shnpyT4hA==",
+      "license": "MIT",
+      "dependencies": {
+        "@vue/devtools-api": "^6.6.3",
+        "vue-demi": "^0.14.10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/posva"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.4.0",
+        "typescript": ">=4.4.4",
+        "vue": "^2.6.14 || ^3.5.11"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        },
+        "typescript": {
+          "optional": true
+        }
+      }
+    },
+    "node_modules/pinia/node_modules/vue-demi": {
+      "version": "0.14.10",
+      "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+      "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+      "hasInstallScript": true,
+      "license": "MIT",
+      "bin": {
+        "vue-demi-fix": "bin/vue-demi-fix.js",
+        "vue-demi-switch": "bin/vue-demi-switch.js"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/antfu"
+      },
+      "peerDependencies": {
+        "@vue/composition-api": "^1.0.0-rc.1",
+        "vue": "^3.0.0-0 || ^2.6.0"
+      },
+      "peerDependenciesMeta": {
+        "@vue/composition-api": {
+          "optional": true
+        }
+      }
+    },
     "node_modules/pkg-dir": {
       "version": "5.0.0",
       "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
@@ -4644,9 +4757,9 @@
       }
     },
     "node_modules/postcss": {
-      "version": "8.4.38",
-      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz",
-      "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==",
+      "version": "8.4.49",
+      "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz",
+      "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==",
       "funding": [
         {
           "type": "opencollective",
@@ -4664,8 +4777,8 @@
       "license": "MIT",
       "dependencies": {
         "nanoid": "^3.3.7",
-        "picocolors": "^1.0.0",
-        "source-map-js": "^1.2.0"
+        "picocolors": "^1.1.1",
+        "source-map-js": "^1.2.1"
       },
       "engines": {
         "node": "^10 || ^12 || >=14"
@@ -5033,12 +5146,12 @@
       }
     },
     "node_modules/rollup": {
-      "version": "4.18.0",
-      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.18.0.tgz",
-      "integrity": "sha512-QmJz14PX3rzbJCN1SG4Xe/bAAX2a6NpCP8ab2vfu2GiUr8AQcr2nCV/oEO3yneFarB67zk8ShlIyWb2LGTb3Sg==",
+      "version": "4.27.4",
+      "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.27.4.tgz",
+      "integrity": "sha512-RLKxqHEMjh/RGLsDxAEsaLO3mWgyoU6x9w6n1ikAzet4B3gI2/3yP6PWY2p9QzRTh6MfEIXB3MwsOY0Iv3vNrw==",
       "license": "MIT",
       "dependencies": {
-        "@types/estree": "1.0.5"
+        "@types/estree": "1.0.6"
       },
       "bin": {
         "rollup": "dist/bin/rollup"
@@ -5048,22 +5161,24 @@
         "npm": ">=8.0.0"
       },
       "optionalDependencies": {
-        "@rollup/rollup-android-arm-eabi": "4.18.0",
-        "@rollup/rollup-android-arm64": "4.18.0",
-        "@rollup/rollup-darwin-arm64": "4.18.0",
-        "@rollup/rollup-darwin-x64": "4.18.0",
-        "@rollup/rollup-linux-arm-gnueabihf": "4.18.0",
-        "@rollup/rollup-linux-arm-musleabihf": "4.18.0",
-        "@rollup/rollup-linux-arm64-gnu": "4.18.0",
-        "@rollup/rollup-linux-arm64-musl": "4.18.0",
-        "@rollup/rollup-linux-powerpc64le-gnu": "4.18.0",
-        "@rollup/rollup-linux-riscv64-gnu": "4.18.0",
-        "@rollup/rollup-linux-s390x-gnu": "4.18.0",
-        "@rollup/rollup-linux-x64-gnu": "4.18.0",
-        "@rollup/rollup-linux-x64-musl": "4.18.0",
-        "@rollup/rollup-win32-arm64-msvc": "4.18.0",
-        "@rollup/rollup-win32-ia32-msvc": "4.18.0",
-        "@rollup/rollup-win32-x64-msvc": "4.18.0",
+        "@rollup/rollup-android-arm-eabi": "4.27.4",
+        "@rollup/rollup-android-arm64": "4.27.4",
+        "@rollup/rollup-darwin-arm64": "4.27.4",
+        "@rollup/rollup-darwin-x64": "4.27.4",
+        "@rollup/rollup-freebsd-arm64": "4.27.4",
+        "@rollup/rollup-freebsd-x64": "4.27.4",
+        "@rollup/rollup-linux-arm-gnueabihf": "4.27.4",
+        "@rollup/rollup-linux-arm-musleabihf": "4.27.4",
+        "@rollup/rollup-linux-arm64-gnu": "4.27.4",
+        "@rollup/rollup-linux-arm64-musl": "4.27.4",
+        "@rollup/rollup-linux-powerpc64le-gnu": "4.27.4",
+        "@rollup/rollup-linux-riscv64-gnu": "4.27.4",
+        "@rollup/rollup-linux-s390x-gnu": "4.27.4",
+        "@rollup/rollup-linux-x64-gnu": "4.27.4",
+        "@rollup/rollup-linux-x64-musl": "4.27.4",
+        "@rollup/rollup-win32-arm64-msvc": "4.27.4",
+        "@rollup/rollup-win32-ia32-msvc": "4.27.4",
+        "@rollup/rollup-win32-x64-msvc": "4.27.4",
         "fsevents": "~2.3.2"
       }
     },
@@ -5236,9 +5351,9 @@
       }
     },
     "node_modules/source-map-js": {
-      "version": "1.2.0",
-      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz",
-      "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==",
+      "version": "1.2.1",
+      "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+      "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
       "license": "BSD-3-Clause",
       "engines": {
         "node": ">=0.10.0"
@@ -5654,14 +5769,14 @@
       "license": "MIT"
     },
     "node_modules/vite": {
-      "version": "5.3.1",
-      "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.1.tgz",
-      "integrity": "sha512-XBmSKRLXLxiaPYamLv3/hnP/KXDai1NDexN0FpkTaZXTfycHvkRHoenpgl/fvuK/kPbB6xAgoyiryAhQNxYmAQ==",
+      "version": "5.4.11",
+      "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz",
+      "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==",
       "license": "MIT",
       "dependencies": {
         "esbuild": "^0.21.3",
-        "postcss": "^8.4.38",
-        "rollup": "^4.13.0"
+        "postcss": "^8.4.43",
+        "rollup": "^4.20.0"
       },
       "bin": {
         "vite": "bin/vite.js"
@@ -5680,6 +5795,7 @@
         "less": "*",
         "lightningcss": "^1.21.0",
         "sass": "*",
+        "sass-embedded": "*",
         "stylus": "*",
         "sugarss": "*",
         "terser": "^5.4.0"
@@ -5697,6 +5813,9 @@
         "sass": {
           "optional": true
         },
+        "sass-embedded": {
+          "optional": true
+        },
         "stylus": {
           "optional": true
         },
@@ -5738,16 +5857,16 @@
       "license": "MIT"
     },
     "node_modules/vue": {
-      "version": "3.4.29",
-      "resolved": "https://registry.npmjs.org/vue/-/vue-3.4.29.tgz",
-      "integrity": "sha512-8QUYfRcYzNlYuzKPfge1UWC6nF9ym0lx7mpGVPJYNhddxEf3DD0+kU07NTL0sXuiT2HuJuKr/iEO8WvXvT0RSQ==",
+      "version": "3.5.13",
+      "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
+      "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
       "license": "MIT",
       "dependencies": {
-        "@vue/compiler-dom": "3.4.29",
-        "@vue/compiler-sfc": "3.4.29",
-        "@vue/runtime-dom": "3.4.29",
-        "@vue/server-renderer": "3.4.29",
-        "@vue/shared": "3.4.29"
+        "@vue/compiler-dom": "3.5.13",
+        "@vue/compiler-sfc": "3.5.13",
+        "@vue/runtime-dom": "3.5.13",
+        "@vue/server-renderer": "3.5.13",
+        "@vue/shared": "3.5.13"
       },
       "peerDependencies": {
         "typescript": "*"
@@ -5827,13 +5946,13 @@
       }
     },
     "node_modules/vue-i18n": {
-      "version": "9.13.1",
-      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.13.1.tgz",
-      "integrity": "sha512-mh0GIxx0wPtPlcB1q4k277y0iKgo25xmDPWioVVYanjPufDBpvu5ySTjP5wOrSvlYQ2m1xI+CFhGdauv/61uQg==",
+      "version": "9.14.2",
+      "resolved": "https://registry.npmjs.org/vue-i18n/-/vue-i18n-9.14.2.tgz",
+      "integrity": "sha512-JK9Pm80OqssGJU2Y6F7DcM8RFHqVG4WkuCqOZTVsXkEzZME7ABejAUqUdA931zEBedc4thBgSUWxeQh4uocJAQ==",
       "license": "MIT",
       "dependencies": {
-        "@intlify/core-base": "9.13.1",
-        "@intlify/shared": "9.13.1",
+        "@intlify/core-base": "9.14.2",
+        "@intlify/shared": "9.14.2",
         "@vue/devtools-api": "^6.5.0"
       },
       "engines": {
diff --git a/frontend/package.json b/frontend/package.json
index fac1193011a54b74cff4affa8e8fbf29238d3357..2c66b946800dcfd415823f0f2b67ccf1734f4b64 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -22,6 +22,7 @@
     "html2canvas": "^1.4.1",
     "jspdf": "^2.5.1",
     "marked": "^13.0.0",
+    "pinia": "^2.2.6",
     "plotly.js-basic-dist-min": "^2.33.0",
     "plotly.js-locales": "^2.33.0",
     "proj4": "^2.11.0",
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
index c7051b68b4a505d78d1a26aa2051ebbf077dbfcf..dc35720e4ddce82253229829889cd3b4b8f75c25 100644
--- a/frontend/src/App.vue
+++ b/frontend/src/App.vue
@@ -24,11 +24,6 @@ export default defineComponent({
 
 <style>
 
-:root {
-  --header-height: 60px;
-  --sidenav-width: 360px;
-}
-
 html {
   scrollbar-width: none;
   overflow: hidden;
diff --git a/frontend/src/assets/theme/blue.css b/frontend/src/assets/theme/blue.css
new file mode 100644
index 0000000000000000000000000000000000000000..d520640caa3bce26de6be6da87123bb9ea5aa537
--- /dev/null
+++ b/frontend/src/assets/theme/blue.css
@@ -0,0 +1,38 @@
+:root {
+  --display-logo: none;
+
+  --header-height: 40px;
+  --header-offset: 40px;
+  --controls-offset: 0px;
+
+  --sidenav-width: 360px;
+
+  --first-background-color: #003e6e;
+  --second-background-color: rgb(225, 232, 238);
+  --third-background-color: white;
+
+  --header-text-color: lightblue;
+  --header-text-highlight-color: white;
+
+  --active-background-color: #003e6e;
+
+  --main-text-color: #003e6e;
+  --outline-color: darkgray;
+
+  --standard-point-data-color: #003e6e;
+
+  --cookie-modal-background: rgba(51, 204, 255, 0.3);
+
+  --ol-background-color: rgb(225, 232, 238) !important;
+  /*--ol-accent-background-color*/
+  /*--ol-subtle-background-color*/
+  --ol-partial-background-color: rgb(225, 232, 238) !important;
+  --ol-subtle-foreground-color: #003e6e !important;
+  --ol-foreground-color: #003e6e !important;
+  /*--ol-brand-color*/
+
+  --chart-color-1: var(--main-color);
+  --chart-color-2: #0091ff;
+  --chart-color-3: #80c8ff;
+  --chart-color-4: #e6f4ff;
+}
\ No newline at end of file
diff --git a/frontend/src/assets/theme/cyan_theme.css b/frontend/src/assets/theme/cyan_theme.css
deleted file mode 100644
index 1bacdef7b7196c6414e18b22dd203ec169c0f5da..0000000000000000000000000000000000000000
--- a/frontend/src/assets/theme/cyan_theme.css
+++ /dev/null
@@ -1,18 +0,0 @@
-:root {
-  --main-color: #139bb5;
-
-  --cookie-modal-background: rgba(51, 204, 255, 0.3);
-
-  --ol-background-color: var(--main-color);
-  /*--ol-accent-background-color*/
-  /*--ol-subtle-background-color*/
-  --ol-partial-background-color: #139bb5a1;
-  --ol-subtle-foreground-color: white;
-  --ol-foreground-color: white;
-  /*--ol-brand-color*/
-
-  --chart-color-1: #0a4f5c;
-  --chart-color-2: #1189a2;
-  --chart-color-3: #18c4e7;
-  --chart-color-4: #8ce2f3;
-}
\ No newline at end of file
diff --git a/frontend/src/assets/theme/dark.css b/frontend/src/assets/theme/dark.css
new file mode 100644
index 0000000000000000000000000000000000000000..dbf963f2611f2053f21fa26b30e072bfd90bce00
--- /dev/null
+++ b/frontend/src/assets/theme/dark.css
@@ -0,0 +1,38 @@
+:root {
+  --display-logo: none;
+
+  --header-height: 40px;
+  --header-offset: 0px;
+  --controls-offset: 40px;
+
+  --sidenav-width: 360px;
+
+  --first-background-color: rgba(10, 10, 10, 0.1);
+  --second-background-color: #606060;
+  --third-background-color: rgba(255, 255, 255, 0);
+
+  --header-text-color: #3a3a3a;
+  --header-text-highlight-color: white;
+
+  --active-background-color: #3a3a3a;
+
+  --main-text-color: white;
+  --outline-color: darkgray;
+
+  --standard-point-data-color: #003e6e;
+
+  --cookie-modal-background: rgba(51, 204, 255, 0.3);
+
+  --ol-background-color: rgba(76, 76, 76, 0.85) !important;
+  /*--ol-accent-background-color*/
+  /*--ol-subtle-background-color*/
+  --ol-partial-background-color: rgba(76, 76, 76, 0.85) !important;
+  --ol-subtle-foreground-color: white !important;
+  --ol-foreground-color: white !important;
+  /*--ol-brand-color*/
+
+  --chart-color-1: red;
+  --chart-color-2: #0091ff;
+  --chart-color-3: #80c8ff;
+  --chart-color-4: #e6f4ff;
+}
\ No newline at end of file
diff --git a/frontend/public/blue_theme.css b/frontend/src/assets/theme/default.css
similarity index 52%
rename from frontend/public/blue_theme.css
rename to frontend/src/assets/theme/default.css
index f038bd1efa8f55616cc93a4ca1ac94a169e08dcb..d48820d53cce759efdbcb915f8c27d97dcea6784 100644
--- a/frontend/public/blue_theme.css
+++ b/frontend/src/assets/theme/default.css
@@ -1,5 +1,25 @@
 :root {
-  --main-color: #00589C;
+  --display-logo: inline-block;
+
+  --header-height: 60px;
+  --header-offset: 60px;
+  --controls-offset: 0px;
+
+  --sidenav-width: 360px;
+
+  --first-background-color: white;
+  --second-background-color: white;
+  --third-background-color: white;
+
+  --header-text-color: #3a3a3a;
+  --header-text-highlight-color: white;
+
+  --active-background-color: #00589C;
+
+  --main-text-color: #3a3a3a;
+  --outline-color: #dbdbdb;
+
+  --standard-point-data-color: #00589C;
 
   --cookie-modal-background: rgba(51, 204, 255, 0.3);
 
diff --git a/frontend/src/assets/theme/green_theme.css b/frontend/src/assets/theme/green_theme.css
deleted file mode 100644
index babcf608523526f0e44b930f4a64765a0311da62..0000000000000000000000000000000000000000
--- a/frontend/src/assets/theme/green_theme.css
+++ /dev/null
@@ -1,18 +0,0 @@
-:root {
-  --main-color: #1d7431;
-
-  --cookie-modal-background: rgba(51, 204, 255, 0.3);
-
-  --ol-background-color: rgba(73,73,73,.85);
-  /*--ol-accent-background-color*/
-  /*--ol-subtle-background-color*/
-  --ol-partial-background-color: rgba(0, 0, 0, 0.5);
-  --ol-subtle-foreground-color: white;
-  --ol-foreground-color: white;
-  /*--ol-brand-color*/
-
-  --chart-color-1: var(--main-color);
-  --chart-color-2: #484848;
-  --chart-color-3: #8d8d8d;
-  --chart-color-4: #d7d7d7;
-}
\ No newline at end of file
diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue
index 5e1749ce3534650b6d46bea8ac4b04a453832678..b4c1f6ca142819a65aade2512201345ee031e101 100644
--- a/frontend/src/components/AppHeader.vue
+++ b/frontend/src/components/AppHeader.vue
@@ -170,7 +170,7 @@ export default defineComponent({
     z-index: 20;
     top: 0;
     left: 0;
-    background: white;
+    background: var(--first-background-color);
     width: 100%;
     border-bottom: 1px solid lightgray;
 }
@@ -186,6 +186,7 @@ export default defineComponent({
 }
 
 #titleText {
+  color: var(--header-text-color);
   position: absolute;
   top: 50%;
   transform: translate(0%, -50%);
@@ -206,6 +207,7 @@ export default defineComponent({
 .fullscreen-control-bar {
   display: inline-block;
   position: inherit;
+  line-height: calc(var(--header-height) - 8px);
 }
 
 </style>
diff --git a/frontend/src/components/MapView.vue b/frontend/src/components/MapView.vue
index 0b7a07386912dd938f66608f9208136f73e602fd..d8522e467d9ddd60382c3de783e6f28893c1bd43 100644
--- a/frontend/src/components/MapView.vue
+++ b/frontend/src/components/MapView.vue
@@ -17,7 +17,8 @@
       :area-names="apiAreaNames"
       :backendUrl="backendUrl"
       :menuSidenavToggle="menuSidenavToggle"
-      :base-map="projectInfo.base_map"
+      :base-layer="projectInfo.base_layer"
+      :is-base-layer-gray="projectInfo.is_base_layer_gray"
       :enable-print-function="projectInfo.enable_print_function"
       :project-print-annotation="projectInfo.print_annotation"
       v-if="apiAreaNames.length > 0"
@@ -28,12 +29,12 @@
 <script lang="ts">
 import AppHeader from "./AppHeader.vue";
 import MapComponent from "./map/MapComponent.vue";
-import { Coordinate } from "openlayers";
 import {defineComponent, ref } from "vue";
 import {fetchAreaNames, fetchProject} from "@/composables/fetchDataFromBackend.ts";
 import {useRoute} from "vue-router";
 import {ProjectInfo} from "@/types";
 import proj4 from "proj4";
+import {Coordinate} from "@/types";
 
 export default defineComponent({
   components: {
@@ -46,14 +47,20 @@ export default defineComponent({
     }
   },
   watch: {
-    'projectInfo': function(projectInfo: ProjectInfo) {
-      this.teaser = projectInfo.teaser
-      this.zoom = Number(projectInfo.default_zoom)
+    'projectInfo': function(val: ProjectInfo) {
+      this.teaser = val.teaser
+      this.zoom = Number(val.default_zoom)
 
-      const mapCenter = [Number(projectInfo.default_lon), Number(projectInfo.default_lat)]
+      const mapCenter = [Number(val.default_lon), Number(val.default_lat)]
       this.center =  proj4('WGS84', 'EPSG:3857', mapCenter)
 
-      // import(/* @vite-ignore */"@/assets/theme/" + projectInfo.theme + "_theme.css")
+      if (val.theme === 'blue') {
+        import('@/assets/theme/blue.css');
+      } else if (val.theme === 'dark') {
+        import('@/assets/theme/dark.css');
+      } else {
+        import('@/assets/theme/default.css');
+      }
     }
   },
   setup() {
diff --git a/frontend/src/components/header/AppLink.vue b/frontend/src/components/header/AppLink.vue
index 52f3f1c685141a892e993594912ca67da7f79243..9941e7929a84a230ae601b6d69865b6099ddbc51 100644
--- a/frontend/src/components/header/AppLink.vue
+++ b/frontend/src/components/header/AppLink.vue
@@ -77,14 +77,23 @@ export default defineComponent({
 <style scoped>
 
 .logo {
-  display: inline-block;
+  display: var(--display-logo);
   margin: 5px 10px;
 }
 
 .page-link {
   vertical-align: middle !important;
-  margin-top: -2px !important;
   width: 180px;
+  background: var(--second-background-color);
+  color: var(--main-text-color);
+}
+
+.simple-link {
+  color: var(--header-text-color);
+}
+
+.simple-link:hover {
+  color: var(--header-text-highlight-color);
 }
 
 .ufz_logo {
@@ -101,4 +110,8 @@ export default defineComponent({
   max-height:100%;
 }
 
+.float-left {
+  float: left;
+}
+
 </style>
\ No newline at end of file
diff --git a/frontend/src/components/header/RightControls.vue b/frontend/src/components/header/RightControls.vue
index 93bc37cf3f5c08039eb4e0fed03a277ddee8d9b3..b458befa71551fb60e7a419b6103c493d4880243 100644
--- a/frontend/src/components/header/RightControls.vue
+++ b/frontend/src/components/header/RightControls.vue
@@ -3,17 +3,17 @@
 <div class="is-inline-block lineHeightHeader" style="float: right">
 
   <AppLink
-    styles="is-size-7 is-small mr-4 is-inline-block"
+    styles="is-size-7 is-small mr-4 is-inline-block simple-link float-left"
     target="imprint"
     label="imprint"
   ></AppLink>
 
   <!-- language switcher-->
 
-  <div class="dropdown is-right mr-3" style="margin-top: 2px" v-if="hasEnglishVersion">
+  <div class="dropdown is-right mr-3" v-if="hasEnglishVersion">
 
     <div class="dropdown-trigger is-size-4" @click="toggleLanguages">
-      <font-awesome-icon :icon="['fas', 'language']" :style="[showMenuLanguages ? 'color: var(--main-color);' : 'color: #4a4a4a;']" />
+      <font-awesome-icon :icon="['fas', 'language']" class="langIcon" :style="[showMenuLanguages ? 'color: var(--header-text-highlight-color);' : 'color: var(--header-text-color);']" />
     </div>
 
     <div
@@ -78,4 +78,8 @@ export default defineComponent({
     right: 0;
 }
 
+.langIcon:hover {
+  color: var(--header-text-highlight-color) !important;
+}
+
 </style>
\ No newline at end of file
diff --git a/frontend/src/components/map/LayerMenu.vue b/frontend/src/components/map/LayerMenu.vue
index 3501d56ae2d703148b287e403178a0eb830cec15..3c04e184258c2f0165a99400232bdd2273f9e037 100644
--- a/frontend/src/components/map/LayerMenu.vue
+++ b/frontend/src/components/map/LayerMenu.vue
@@ -1,13 +1,13 @@
 <template>
 
-  <div class="ml-3 mr-3" :class="[isNonWms(type) && showSwiper ? 'menu-disabled' : '']">
+  <div class="ml-3 mr-3" :class="[!isWmsLayerMenu && showSwiper ? 'menu-disabled' : '']">
     <div
         :class="[showSwiper ? 'menuSmall shrinking-object' : 'menuLarge growing-object']"
         class="menu innerLayerMenu"
     >
       <ul class="menu-list">
         <template v-for="(layer, index) in selectableLayers" :key="'active' + index" >
-          <li v-if="layer.type == type">
+          <li v-if="layerTypes?.indexOf(layer.type) !== -1">
             <a
               @click="setLayer(layer)"
               class="button is-small is-clipped menuItem"
@@ -31,49 +31,46 @@ import { Layer } from "@/types";
 export default defineComponent({
   props: {
     selectableLayers: Array as PropType<Layer[]>,
-    type: String,
+    layerTypes: Array as PropType<String[]>,
     label: String,
     layerToLoad: Number,
     showSwiper: Boolean
   },
   data() {
     return {
-      selectedLayerId: this.$props.layerToLoad as number,
+      selectedLayerId: this.$props.layerToLoad as number
     };
   },
   watch: {
     'showSwiper': function (val) {
-      if(this.isNonWms(this.$props.type) && val == true) {
+      if(!this.isWmsLayerMenu && val == true) {
         this.selectedLayerId = 0
       }
     },
     'layerToLoad': function(id: number|undefined) {
-      if (this.$props.type === 'sta' && id === undefined) {
-        return
-      }
       this.selectedLayerId = id
     }
   },
   computed: {
-    'areOtherLayersAvailable'() {
+    'areLayersInMenuUnsettable'() {
       for (const layer of this.selectableLayers) {
-        if (layer.type !== this.$props.type) {
+        if (layer.type !== 'wms') {
           return true
         }
       }
       return false
+    },
+    'isWmsLayerMenu'() {
+      return this.$props.types?.indexOf('wms') !== -1;
     }
   },
   methods: {
-    isNonWms(type: string | undefined) {
-      return (type == 'sta' || type == 'geojson')
-    },
     setLayer(layer: Layer): void {
       if (this.selectedLayerId !== layer.id) {
         this.selectedLayerId = layer.id
         this.$emit("layerChanged", layer)
       } else {
-        if (this.areOtherLayersAvailable) {
+        if (this.areLayersInMenuUnsettable) {
           this.selectedLayerId = 0
           this.$emit("layerUnset")
         }
@@ -137,6 +134,8 @@ export default defineComponent({
 }
 
 .menuItem {
+  background: var(--second-background-color);
+  color: var(--main-text-color);
   word-break: break-word;
   height: auto !important;
   white-space: inherit !important;
diff --git a/frontend/src/components/map/MapComponent.ts b/frontend/src/components/map/MapComponent.ts
index ce202d94447ac8293f9f146427c625f1f93494e6..8f3dfc844f9a13b7e33fb764384ddad27461bc53 100644
--- a/frontend/src/components/map/MapComponent.ts
+++ b/frontend/src/components/map/MapComponent.ts
@@ -1,3 +1,5 @@
+// noinspection TypeScriptValidateTypes
+
 import {defineComponent, onMounted, PropType, ref} from "vue"
 import axios from 'axios'
 import proj4 from 'proj4'
@@ -11,7 +13,6 @@ import { getLastTimeStepIfExists, setPageHeight } from "@/composables/utils"
 import { countryCenterCoords } from "@/composables/countries"
 import shp, { FeatureCollectionWithFilename, ShpJSBuffer } from 'shpjs'
 import OpacityControl from "./wms/OpacityControl.vue";
-import StaLayer from "./sta/StaLayer.vue";
 import AggregationWidget from "./wms/AggregationWidget.vue";
 import LayerMenu from "./LayerMenu.vue";
 import TimeSeriesChart from "./TimeSeriesChart.vue";
@@ -19,17 +20,14 @@ import MapPopup from "./MapPopup.vue";
 import GeojsonLayer from "./geojson/GeojsonLayer.vue";
 import {convertTime} from "@/composables/time_utils";
 import ProjectTeaser from "./ProjectTeaser.vue";
-import StaControl from "./sta/StaControl.vue";
 import StickyPopup from "./StickyPopup.vue";
 
 import {
     AggregationEvent,
     Area, AreaCache,
     AreaName,
-    BoundingBox,
     Coordinate,
     CovJsonSingleResult,
-    DataCollection,
     GEOJSONLayer,
     Layer,
     LayerFilter,
@@ -38,18 +36,24 @@ import {
     Vue3TileLayer,
     WMSLayer
 } from "@/types"
-import {ObservedProperty, STAFilterParams, STALayer, STAObservedProperty, StaPopupInfo, StaThing} from "@/types/sta";
+import {STALayer, WFSLayer} from "@/types/vector";
 import UncertaintyLayer from "@/components/map/geojson/UncertaintyLayer.vue";
 import CountryLayer from "@/components/map/geojson/CountryLayer.vue";
 import StickyPopupControl from "@/components/map/wms/StickyPopupControl.vue";
 import MapPrintControl from "@/components/map/MapPrintControl.vue";
 import FilterMenu from "@/components/map/FilterMenu.vue";
 
+import {TimeseriesStore, useTimeseriesStore} from '@/stores/timeseries.ts'
+import {FeatureStore, useFeatureStore} from "@/stores/feature.ts";
+import VectorLayer from "@/components/map/vector/VectorLayer.vue";
+import VectorControl from "@/components/map/vector/VectorControl.vue";
+
 proj4.defs("EPSG:32632", "+proj=utm +zone=32 +datum=WGS84 +units=m +no_defs")
 proj4.defs("EPSG:3035","+proj=laea +lat_0=52 +lon_0=10 +x_0=4321000 +y_0=3210000 +ellps=GRS80 +towgs84=0,0,0,0,0,0,0 +units=m +no_defs +type=crs");
 
 export default defineComponent({
     components: {
+        VectorLayer,
         MapPrintControl,
         CountryLayer,
         UncertaintyLayer,
@@ -57,9 +61,8 @@ export default defineComponent({
         MapPopup,
         LayerMenu,
         AggregationWidget,
-        StaLayer,
         FilterMenu,
-        StaControl,
+        VectorControl,
         MapLegend,
         OpacityControl,
         GeojsonLayer,
@@ -84,7 +87,8 @@ export default defineComponent({
         menuSidenavToggle: Boolean,
         enablePrintFunction: Boolean,
         projectPrintAnnotation: String,
-        baseMap: String
+        baseLayer: String,
+        isBaseLayerGray: Boolean
     },
     data() {
         return {
@@ -101,7 +105,6 @@ export default defineComponent({
             isWms1Loading: false as boolean,
             isWms2Loading: false as boolean,
             isVectorDataLoading: false as boolean,
-            isChartLoading: false as boolean,
 
             isMapVisible: true as boolean,
             areCountriesVisible: false as boolean,
@@ -119,7 +122,8 @@ export default defineComponent({
             significanceUncertainty: undefined as string | undefined,
             showStabilityUncertainty: true as boolean,
             showSignificanceUncertainty: true as boolean,
-            showStaLayer: false as boolean,
+            showVectorLayer: false as boolean,
+            showVectorControl: true as boolean,
 
             showSideNav: false as boolean,
             showAreaMenu: false as boolean,
@@ -130,7 +134,6 @@ export default defineComponent({
             showLayerInfo: false as boolean,
             showShapefileInfo: false as boolean,
             showMainLayerTimeRange: true as boolean,
-            showStaControl: true as boolean,
             layerInfoTitle: '' as string,
             layerInfoText: '' as string,
             mainFilters: {} as LayerFilter,
@@ -142,15 +145,8 @@ export default defineComponent({
             comparisonLayerOpacity: 0.5 as number,
             isOpaque: false as boolean,
 
-            isStaRequest: false as boolean,
             staLayer: {} as STALayer|null,
-            popupInfos: [] as StaPopupInfo[],
-            thingProperties: [] as ObservedProperty[],
-            staObsProperty: {} as ObservedProperty|undefined,
-            staInput: {} as STAFilterParams,
-            showStaFilter: false as boolean,
-            staThing: undefined as StaThing | undefined,
-            currentBoundingBox: [[12.5,51.4], [13.3, 51.35]] as BoundingBox,
+            wfsLayer: null as WFSLayer|null,
 
             showPrintDialog: false as boolean,
             showPointRequestMessage: false as boolean,
@@ -158,8 +154,10 @@ export default defineComponent({
             hasAreaWmsLayers: false as boolean,
             hasAreaStaLayers: false as boolean,
             hasAreaGeojsonLayers: false as boolean,
+            hasAreaWfsLayers: false as boolean,
 
-            dataCollection: {} as DataCollection,
+            featureStore: {} as FeatureStore,
+            timeseriesStore: {} as TimeseriesStore,
             clickedLayer: {} as WMSLayer|null,
             clickedLayerTime: '' as string,
             clickedCoord: [0, 0] as Coordinate,
@@ -215,24 +213,16 @@ export default defineComponent({
         attribution() {
             return '© Bundesamt für Kartographie und Geodäsie ' + new Date().getFullYear()
         },
-        showStationInformation() {
-            return (this.staLayer != null)
-        },
         showTimeSeriesButton() {
-            if (this.isChartLoading || this.showChart) {
+            if (this.timeseriesStore.isChartLoading) {
                 return false
             }
 
-            if (this.dataCollection.timeSeriesCollection) {
-                if (this.dataCollection.timeSeriesCollection.length == 0) {
-                    return false
-                }
-                if (!this.isWideScreen || this.staLayer) {
-                    return true
-                }
-                if (!this.showChart) {
+            if (this.timeseriesStore.dataCollection.timeSeriesCollection.length > 0) {
+                if (!this.isWideScreen) {
                     return true
                 }
+                return !this.showChart
             }
             return false
         },
@@ -241,6 +231,15 @@ export default defineComponent({
         },
         isClearDisabled() {
             return (this.showMainFilter && this.isMainFilterCleared) || (this.showComparisonFilter && this.isComparisonFilterCleared)
+        },
+        vectorLayerId() {
+            if (this.staLayer) {
+                return this.staLayer.id
+            }
+            if (this.wfsLayer) {
+                return this.wfsLayer.id
+            }
+            return undefined
         }
     },
     methods: {
@@ -287,6 +286,13 @@ export default defineComponent({
 
             this.isSidenavLoading = true
 
+            this.unsetMainWmsLayer()
+            this.unsetVectorLayer()
+            this.unsetGeojsonLayer()
+            this.timeseriesStore.clickedLayer = null
+
+            this.showPrintDialog = false
+
             this.requestArea(areaId)
                 .then((area: Area) => {
                     this.activeArea = area
@@ -295,12 +301,6 @@ export default defineComponent({
                         this.requestFilters(area)
                     }
 
-                    this.unsetMainWmsLayer()
-                    this.unsetStaLayer()
-                    this.unsetGeojsonLayer()
-
-                    this.showPrintDialog = false
-
                     if (layerId === 0) {
                         this.setLayer(this.getDefaultLayer(area))
                     } else {
@@ -321,10 +321,9 @@ export default defineComponent({
                         if (layer.type === 'geojson') {
                             this.hasAreaGeojsonLayers = true
                         }
-                    }
-
-                    if (!this.hasAreaStaLayers && !this.hasAreaGeojsonLayers) {
-                        this.setComparisonLayer(this.activeArea.layers[0])
+                        if (layer.type === 'wfs') {
+                            this.hasAreaWfsLayers = true
+                        }
                     }
 
                     this.clearMainFilter()
@@ -340,9 +339,6 @@ export default defineComponent({
                 })
                 .then(() => {
                     this.isSidenavLoading = false
-                    if (this.mainLayer) {
-                        this.updateURL(this.mainLayer)
-                    }
                 })
         },
         toggleSwiper() {
@@ -352,10 +348,21 @@ export default defineComponent({
             this.areDistrictsVisible = false
             this.isShapefileVisible = false
             this.closeAggregationWidget()
-            this.showSwiper = !this.showSwiper
 
-            this.showStaLayer = !this.showSwiper
             this.showComparisonFilter = false
+
+            if (!this.showSwiper) {
+                this.layerList.push(this.$refs.wmsLayer1.olImageLayer.imageLayer)
+                this.layerList.push(this.$refs.wmsLayer2.olImageLayer.imageLayer)
+
+                this.$nextTick(() => {
+                    this.showSwiper = true
+                    this.showVectorLayer = false
+                });
+            } else {
+                this.showSwiper = false
+                this.showVectorLayer = true
+            }
         },
         toggleOpacity(deactivateOpacity = false) {
             if (this.mainLayerOpacity === 1 || deactivateOpacity) {
@@ -401,14 +408,17 @@ export default defineComponent({
         },
         setLayer(layer: Layer) {
             if (layer.type == 'wms') {
-                this.setMainLayer(layer as WMSLayer, false)
+                this.setMainLayer(layer as WMSLayer)
+                this.setComparisonLayer(layer as WMSLayer)
             } else if (layer.type == 'sta') {
                 this.setStaLayer(layer as STALayer)
             } else if (layer.type == 'geojson') {
                 this.setGeojsonLayer(layer as GEOJSONLayer)
+            } else if (layer.type == 'wfs') {
+                this.setWfsLayer(layer as WFSLayer)
             }
         },
-        setMainLayer(mainLayer: WMSLayer, updateUrl = true) {
+        setMainLayer(mainLayer: WMSLayer) {
             this.showPrintDialog = false
             this.showPopup = false
             this.showChart = false
@@ -421,44 +431,70 @@ export default defineComponent({
             this.aggregationEvent = null
 
             this.setMainLayerUncertainties()
-
             this.setPreviousResults()
-
-            if (updateUrl) {
-                this.updateURL(mainLayer)
+            this.updateURL(mainLayer)
+        },
+        setVectorLayer(layer: Layer) {
+            if (layer.type === 'sta') {
+                this.setStaLayer(layer)
             }
+            if (layer.type === 'wfs') {
+                this.setWfsLayer(layer)
+            }
+            this.updateURL(layer)
         },
         setStaLayer(layer: STALayer) {
-            this.unsetStaLayer()
-            this.showStaLayer = true
-            this.showStaControl = true
+            this.unsetVectorLayer()
+            this.showVectorLayer = true
+            this.showVectorControl = true
             this.showMainLayerTimeRange = false
+
+            this.featureStore.staLayer = layer
+            this.timeseriesStore.staLayer = layer
+            this.updateURL(layer)
             setTimeout(() => this.staLayer = layer, 100)
         },
         setGeojsonLayer(layer: GEOJSONLayer) {
             this.unsetGeojsonLayer()
+            this.updateURL(layer)
             setTimeout(() => this.geojsonLayer = layer, 100)
         },
+        setWfsLayer(layer: WFSLayer) {
+            this.unsetVectorLayer()
+            this.showVectorLayer = true
+            this.showVectorControl = true
+            this.showMainLayerTimeRange = false
+
+            this.featureStore.wfsLayer = layer
+            this.timeseriesStore.wfsLayer = layer
+            this.updateURL(layer)
+            setTimeout(() => { this.wfsLayer = layer }, 100)
+        },
         unsetMainWmsLayer() {
             this.showSwiper = false
             this.mainLayer = null
             this.clickedLayer = null
             this.closeChartAndPopup()
             this.showMainLayerTimeRange = false
-            this.showStaControl = true
+            this.showVectorControl = true
         },
-        unsetStaLayer() {
-            this.staObsProperty = undefined
+        unsetVectorLayer() {
             this.staLayer = null
-            this.showStaLayer = false
-            this.staThing = undefined
-            this.dataCollection = {
-                chartName: '',
-                mapPopupResult: {},
-                timeSeriesCollection: [],
-                singleResultCollection: []
-            }
-            this.showStaControl = false
+            this.wfsLayer = null
+            this.showVectorLayer = false
+
+            this.featureStore.staLayer = null
+            this.featureStore.wfsLayer = null
+            this.featureStore.setSelectedProperty(undefined)
+
+            this.timeseriesStore.staLayer = null
+            this.timeseriesStore.staThing = undefined
+            this.timeseriesStore.wfsLayer = null
+            this.timeseriesStore.setSelectedProperty(undefined)
+
+            this.timeseriesStore.unsetTimeseriesData()
+
+            this.showVectorControl = false
             this.showMainLayerTimeRange = true
             this.closeChartAndPopup()
         },
@@ -491,26 +527,20 @@ export default defineComponent({
 
             this.setMainLayerUncertainties()
         },
-        setStaObsProperty(property: ObservedProperty|undefined) {
+        requestVectorData() {
             this.showChart = false
-            this.staObsProperty = property
-            if (this.staThing) {
-                this.isStaRequest = true
-                this.requestPointData()
+            this.showPopup = true
+            if (this.timeseriesStore.isClickOnFeature) {
+                this.showChart = true
             }
         },
-        setStaObsPropertyAndReload(property: ObservedProperty|undefined) {
-            this.staObsProperty = property
+        reloadStaFeatures() {
             this.closeChartAndPopup()
-            if (property != undefined) {
-                this.isMainFilterCleared = false
-            }
+            this.featureStore.requestStaFeatures()
         },
-        setStaInput(params: STAFilterParams) {
-            this.staInput = params
-            if (params.beginDate !== "" || params.endDate !== "" || params.threshold !== "") {
-                this.isMainFilterCleared = false
-            }
+        reloadWfsFeatures() {
+            this.closeChartAndPopup()
+            this.featureStore.requestWfsFeatures()
         },
         removeShapefile() {
             this.isShapefileVisible = false
@@ -528,7 +558,7 @@ export default defineComponent({
         },
         toggleTimeRange() {
             this.showMainLayerTimeRange = !this.showMainLayerTimeRange
-            this.showStaControl = !this.showMainLayerTimeRange;
+            this.showVectorControl = !this.showMainLayerTimeRange;
         },
         setComparisonLayerTime(value: string) {
             this.comparisonLayerTime = value
@@ -583,6 +613,9 @@ export default defineComponent({
 
             this.$refs.view.animate({zoom: 6.5, center: ger_center})
         },
+        setCenterToCluster(coord: Coordinate) {
+            this.$refs.view.animate({zoom: this.$refs.view.getZoom() + 2, center: coord})
+        },
         requestFilters(area: Area) {
 
             const url = this.backendUrl + 'filter/' + this.$i18n.locale + '/' + area.id
@@ -691,9 +724,9 @@ export default defineComponent({
         setPreviousResults() {
             this.previousClickedCoord = this.clickedCoord
             this.previousClickedCoordWgs84 = this.clickedCoordWgs84
-            if (this.dataCollection.mapPopupResult) {
-                if (this.dataCollection.mapPopupResult.value) {
-                    this.previousSingleResult = this.dataCollection.mapPopupResult
+            if (this.timeseriesStore.dataCollection.mapPopupResult) {
+                if (this.timeseriesStore.dataCollection.mapPopupResult.value) {
+                    this.previousSingleResult = this.timeseriesStore.dataCollection.mapPopupResult
                 }
             }
         },
@@ -730,58 +763,26 @@ export default defineComponent({
                         this.clickedLayerTime = this.comparisonLayerTime
                     }
                 }
-                if (!this.isStaRequest) {
-                    this.requestPointData()
-                }
-            }
-        },
-        requestPointData() {
 
-            const params: string[] = []
+                this.timeseriesStore.clickedLayer = this.clickedLayer
+                this.timeseriesStore.clickedLayerTime = this.clickedLayerTime
+                this.timeseriesStore.clickedCoord = this.clickedCoord
 
-            if (this.clickedLayer) {
-                if (this.clickedLayer.is_point_request_enabled === false) {
-                    this.showPointRequestMessage = true
-                    setTimeout(() => this.showPointRequestMessage = false, 4000)
-                    return
-                }
-                params.push('wmsLayer=' + this.clickedLayer.own_id)
-                params.push('time=' + this.clickedLayerTime)
-                params.push('x=' + this.clickedCoord[0])
-                params.push('y=' + this.clickedCoord[1])
-            }
-
-            if (this.isStaRequest && this.staLayer && this.staThing && this.staObsProperty) {
-                params.push('staLayer=' + this.staLayer.own_id)
-                params.push('staThing=' + this.staThing['@iot.id'])
-                params.push('staObsProperty=' + this.staObsProperty.id)
-            }
+                if (this.mainLayer) {
+                    this.timeseriesStore.requestPointData()
 
-            if (params.length > 0) {
-                this.isChartLoading = true
-
-                const url = import.meta.env.VITE_APP_BACKEND_URL + 'timeseries/' + this.$i18n.locale + '?' + params.join('&')
+                    if (!this.clickedLayer.is_point_request_enabled) {
+                        this.showPointRequestMessage = true
+                        setTimeout(() => this.showPointRequestMessage = false, 4000)
+                    }
+                }
 
-                axios.get(url)
-                    .then(response => {
-                        this.dataCollection = response.data
-                        if (this.dataCollection.mapPopupResult) {
-                            this.setTimeFormatInPopup(this.dataCollection.mapPopupResult)
-                        }
-                    })
-                    .catch((error) => {
-                        console.log(error)
-                    })
-                    .finally(() => {
-                        this.showPopup = true
-                        if (!this.staLayer || this.staLayer && this.staObsProperty) {
-                            if (this.isWideScreen && this.dataCollection.timeSeriesCollection.length > 0) {
-                                this.showChart = true
-                            }
-                        }
-                        this.isChartLoading = false
-                        this.isStaRequest = false
-                    })
+                if (this.mainLayer || this.timeseriesStore.isClickOnFeature) {
+                    this.showPopup = true
+                    if (this.isWideScreen) {
+                        this.showChart = true
+                    }
+                }
             }
         },
         triggerAggregationWidget(layer: WMSLayer, type: string) {
@@ -903,76 +904,6 @@ export default defineComponent({
 
             }.bind(this))
         },
-        setTimeFormatInPopup(result: CovJsonSingleResult) {
-            this.dataCollection.mapPopupResult.time = convertTime(
-                result.time,
-                this.$i18n.locale,
-                result.timeFormat
-            )
-        },
-        setStaThing(thing: StaThing) {
-            this.staThing = thing
-            this.isStaRequest = true
-            this.requestThingProperties(thing)
-            this.requestPointData()
-        },
-        requestThingProperties(thing: StaThing) {
-
-            let url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-obs-properties-by-thing/' + this.staLayer.own_id + '/' + thing['@iot.id']
-
-            axios.get(url)
-                .then(response => {
-                    this.setPopupInfos(thing)
-                    this.setThingProperties(response.data.value)
-                    this.showPopup = true
-                })
-                .catch((error: any) => {
-                    console.log(error)
-                })
-                .finally(() => {
-                    if (this.thingProperties.length == 1) {
-                        this.isStaRequest = true
-                        this.staObsProperty = this.thingProperties[0]
-                        this.requestPointData()
-                    }
-                })
-        },
-        setPopupInfos(thing: StaThing) {
-            this.popupInfos = []
-            for (const key of Object.keys(thing.properties)) {
-                const config = this.staLayer.popupInfoConfig[key]
-                if (config !== undefined) {
-
-                    let infoText = thing.properties[key]
-                    if (infoText instanceof Array) {
-                        infoText = infoText.join(', ')
-                    }
-                    this.popupInfos.push({
-                        label: (this.$i18n.locale === 'en') ? config.name_in_frontend_en : config.name_in_frontend_de,
-                        text: infoText
-                    })
-                }
-            }
-        },
-        setThingProperties(obsProperties: STAObservedProperty[]) {
-            this.thingProperties = []
-
-            const name_prop = (this.$i18n.locale == 'de') ? 'name_de' : 'name_en'   // TODO: switch to name
-
-            for (const obsProperty of obsProperties) {
-
-                let fullName = obsProperty.properties[name_prop]
-                if (!fullName) {
-                    fullName = obsProperty.name
-                }
-
-                this.thingProperties.push({
-                    id: obsProperty['@iot.id'],
-                    fullName: fullName
-                })
-            }
-            this.thingProperties.sort((a: ObservedProperty, b: ObservedProperty) => a.fullName.localeCompare(b.fullName))
-        },
         getCurrentExtent() {
             if (this.$refs.map) {
                 return this.$refs.view.calculateExtent(this.$refs.map.map.getSize())
@@ -982,10 +913,17 @@ export default defineComponent({
         setBoundingBox() {
             const extent = this.getCurrentExtent()
             if (extent) {
-                this.currentBoundingBox = [
-                    proj4('EPSG:3857', 'WGS84', [extent[0], extent[3]]),
-                    proj4('EPSG:3857', 'WGS84', [extent[2], extent[1]]),
+                this.featureStore.currentBoundingBox = [
+                    proj4('EPSG:3857', 'WGS84', [extent[0], extent[1]]),
+                    proj4('EPSG:3857', 'WGS84', [extent[2], extent[3]]),
                 ]
+                if (this.staLayer) {
+                    this.featureStore.requestStaFeatures()
+                }
+
+                if (this.wfsLayer) {
+                    this.featureStore.requestWfsFeatures()
+                }
             }
         },
     },
@@ -1001,6 +939,18 @@ export default defineComponent({
         setPageHeight()
         this.$refs.map.updateSize()
 
+        if (this.isBaseLayerGray) {
+            if (this.baseLayer=='baseMap') {
+                this.$refs.baseLayer.imageLayer.on('postrender', function(event) {
+                     greyscale(event.context);
+                })
+            } else {
+                this.$refs.osmLayer.tileLayer.on('postrender', function(event) {
+                     greyscale(event.context);
+                })
+            }
+        }
+
         window.addEventListener('resize', () => {
             setPageHeight()
             this.$refs.map.updateSize()
@@ -1010,6 +960,9 @@ export default defineComponent({
         if (this.isWideScreen) {
             this.showSideNav = true
         }
+
+        this.featureStore = useFeatureStore()
+        this.timeseriesStore = useTimeseriesStore()
     },
     watch: {
         menuSidenavToggle() {
@@ -1044,35 +997,6 @@ export default defineComponent({
             comparisonLayerStyle.value = style
         }
 
-        //initiate Swiper (with layerList)
-        // TODO: make this somehow better...
-        onMounted(() => {
-            axios.get(import.meta.env.VITE_APP_BACKEND_URL + 'area/de/' + props.areaNames[0].id)
-                .then(response => {
-                    for (const layer of response.data.result.area.layers) {
-                        if (layer.type == 'wms') {
-                            const wmsLayer = layer as WMSLayer
-
-                            mainLayer = ref<WMSLayer>(wmsLayer)
-                            mainLayerTime.value = getLastTimeStepIfExists(wmsLayer)
-                            mainLayerStyle.value = wmsLayer.styles[0]
-                            comparisonLayer = ref<WMSLayer>(wmsLayer)
-                            comparisonLayerTime.value = getLastTimeStepIfExists(wmsLayer)
-                            comparisonLayerStyle.value = wmsLayer.styles[0] as string
-                            return true
-                        }
-                    }
-                })
-                .then(() => {
-                    setTimeout(() => {
-                        if (wmsLayer1.value && wmsLayer2.value) {
-                            layerList.value.push(wmsLayer1.value.olImageLayer.imageLayer)
-                            layerList.value.push(wmsLayer2.value.olImageLayer.imageLayer)
-                        }
-                    }, 200)
-                })
-        })
-
         const map = ref<string>()
 
         return {
@@ -1094,3 +1018,13 @@ export default defineComponent({
         }
     }
 })
+
+function greyscale(context) {
+    const imageData = context.getImageData(0, 0, context.canvas.width, context.canvas.height);
+    const data = imageData.data;
+
+    for (let i=0; i < data.length; i += 4) {
+        data[i] = data[i + 1] = data[i + 2] = (3 * data[i] + 4 * data[i + 1] + data[i + 2]) / 8
+    }
+    context.putImageData(imageData, 0, 0)
+}
\ No newline at end of file
diff --git a/frontend/src/components/map/MapComponent.vue b/frontend/src/components/map/MapComponent.vue
index c77c3c145730870f1900c271ebd1bf19ec848695..7751de42b7ba2cedb03f2492950fa6a9f36bd056 100644
--- a/frontend/src/components/map/MapComponent.vue
+++ b/frontend/src/components/map/MapComponent.vue
@@ -13,7 +13,7 @@
         <div class="block m-3" >
           <div v-if="areaNames.length > 1">
             <div>
-              <button class="button is-small width260" @click="toggleAreaMenu">
+              <button class="button is-small width260 standard-button" @click="toggleAreaMenu">
                 {{ $t("message.selectablesAreas") }}:&nbsp;{{ activeArea.name }}&nbsp;&nbsp;&nbsp;&nbsp;
                 <font-awesome-icon :icon="['fas', 'angle-up']" v-show="showAreaMenu"/>
                 <font-awesome-icon :icon="['fas', 'angle-down']" v-show="!showAreaMenu"/>
@@ -22,13 +22,13 @@
 
             <div v-if="showAreaMenu" class="appearing-object">
               <div v-for="areaName in areaNames" :key="areaName.id">
-                <button class="button is-small width260" :class="[areaName.id === activeArea.id ? 'is-active' : '']" @click="switchArea(areaName.id)">{{ areaName.name }}</button>
+                <button class="button is-small width260" :class="[areaName.id === activeArea.id ? 'is-active' : 'second-color-scheme']" @click="switchArea(areaName.id)">{{ areaName.name }}</button>
               </div>
             </div>
           </div>
 
           <div class="mt-1 mb-6">
-            <button :disabled="showPrintDialog" class="button is-small is-rounded" @click="toggleAreaInfoBox">
+            <button :disabled="showPrintDialog" class="button is-small is-rounded standard-button" @click="toggleAreaInfoBox">
               <font-awesome-icon :icon="['fas', 'circle-info']" size="lg" style="color: #4a4a4a; margin-top: -2px;" />&nbsp;&nbsp;{{ $t("message.showDescription") }}
             </button>
           </div>
@@ -49,17 +49,18 @@
         <div class="mt-3">
           <LayerMenu
               :selectable-layers="selectableMainLayers"
-              :type="'wms'"
+              :layer-types="['wms']"
               :label="'Layer 1:'"
               :show-swiper="showSwiper"
               :layer-to-load="mainLayer?.id"
               @layerChanged="setMainLayer"
               @layerUnset="unsetMainWmsLayer"
+              v-if="hasAreaWmsLayers"
           ></LayerMenu>
 
           <LayerMenu
               :selectable-layers="selectableMainLayers"
-              :type="'geojson'"
+              :layer-types="['geojson']"
               :label="''"
               :show-swiper="showSwiper"
               :layer-to-load="geojsonLayer?.id"
@@ -70,13 +71,13 @@
 
           <LayerMenu
               :selectable-layers="selectableMainLayers"
-              :type="'sta'"
+              :layer-types="['sta', 'wfs']"
               :label="''"
               :show-swiper="showSwiper"
-              :layer-to-load="staLayer?.id"
-              @layerChanged="setStaLayer"
-              @layerUnset="unsetStaLayer"
-              v-if="hasAreaStaLayers"
+              :layer-to-load="vectorLayerId"
+              @layerChanged="setVectorLayer"
+              @layerUnset="unsetVectorLayer"
+              v-if="hasAreaStaLayers || hasAreaWfsLayers"
           ></LayerMenu>
 
           <div v-show="selectableMainLayers?.length==0" id="emptyLayerListText">{{ $t("message.noLayerSelectable") }}</div>
@@ -88,7 +89,7 @@
         <div class="m-1" v-if="activeArea.layers.length > 1 && !hasAreaStaLayers">
           <button
               :disabled="showPrintDialog"
-              class="button is-small"
+              class="button is-small standard-button"
               :class="[showSwiper ? 'is-success' : '']"
               @click="toggleSwiper"
           >
@@ -115,7 +116,7 @@
         <div class="m-3">
           <LayerMenu
               :selectable-layers="selectableComparisonLayers"
-              :type="'wms'"
+              :layer-types="['wms']"
               :label="'Layer 2:'"
               :show-swiper="showSwiper"
               :layer-to-load="comparisonLayer?.id"
@@ -130,7 +131,7 @@
             v-if="(mainLayer?.type == 'wms' && mainLayer.available_methods.length > 0) && !showSwiper"
         >
           <div>
-            <button class="button is-small width260" @click="toggleRegionalSelectables">
+            <button class="button is-small width260 standard-button" @click="toggleRegionalSelectables">
               {{ $t("message.selectablesRegional") }}&nbsp;&nbsp;&nbsp;&nbsp;
               <font-awesome-icon :icon="['fas', 'angle-up']" v-show="showRegionalSelectables"/>
               <font-awesome-icon :icon="['fas', 'angle-down']" v-show="!showRegionalSelectables"/>
@@ -142,7 +143,7 @@
             <div class="m-2" v-if="mainLayer?.type == 'wms' && mainLayer.available_methods.length > 0">
               <button
                   :disabled="showPrintDialog"
-                  class="button is-small appearing-object"
+                  class="button is-small appearing-object standard-button"
                   :class="[areCountriesVisible || areDistrictsVisible ? 'is-success' : '']"
                   @click="toggleCountries"
               >
@@ -156,7 +157,7 @@
               <div class="file is-small is-inline-block is-white appearing-object">
                 <label class="file-label mt-1">
                   <input :disabled="showPrintDialog" class="file-input" type="file" ref="fileInput" name="resume" @change="uploadShapefile">
-                  <span class="file-cta upload-button">
+                  <span class="file-cta upload-button standard-button">
                     <span class="file-label">
                       <font-awesome-icon :icon="['fas', 'upload']" style="margin-top: 2px;"/>
                       &nbsp;{{ $t("message.uploadShapefile") }}
@@ -166,7 +167,7 @@
               </div>
 
               <button
-                  class="button is-small m-1 infoButton appearing-object"
+                  class="button is-small m-1 infoButton appearing-object standard-button"
                   @click="toggleShapefileInfo"
               >
                 <font-awesome-icon :icon="['fas', 'circle-info']" size="lg" style="color: #4a4a4a;" />
@@ -195,7 +196,7 @@
     @singleclick="handleMapClick"
     @moveend="setBoundingBox"
   >
-    <div id="loadingInfo" class="loading-ring" v-show="isSidenavLoading || isWms1Loading || isWms2Loading || isVectorDataLoading || isChartLoading"></div>
+    <div id="loadingInfo" class="loading-ring" v-show="isSidenavLoading || isWms1Loading || isWms2Loading || isVectorDataLoading || timeseriesStore.isChartLoading || featureStore.isLoading"></div>
 
     <ol-view
       ref="view"
@@ -211,7 +212,7 @@
     />
 
     <div v-if="isMapVisible">
-      <ol-image-layer :zIndex="-1" v-if="baseMap=='baseMap'">
+      <ol-image-layer :zIndex="-1" v-if="baseLayer=='baseMap'" ref="baseLayer">
         <ol-source-image-wms
           v-bind="!showPrintDialog && {attributions: attribution}"
           :ratio="1"
@@ -284,16 +285,25 @@
         v-if="mainLayer && mainLayer.styles.length > 0"
       ></MapLegend>
 
-      <StaControl
+
+      <VectorControl
         :layer="staLayer"
-        :obs-property="staObsProperty"
         :is-wms-time-control-active="mainLayer!=undefined"
-        v-if="staLayer && showStaLayer && showStaControl"
-        @sendObsProperty="setStaObsPropertyAndReload"
-        @sendStaFilterParams="setStaInput"
+        v-if="staLayer && showVectorLayer && showVectorControl"
+        @sendVectorFilter="reloadStaFeatures"
+        @sendLayerInfo="toggleLayerInfo"
+        @sendIsVisible="toggleTimeRange"
+      ></VectorControl>
+
+
+      <VectorControl
+        :layer="wfsLayer"
+        :is-wms-time-control-active="mainLayer!=undefined"
+        v-if="wfsLayer && showVectorLayer && showVectorControl"
+        @sendVectorFilter="reloadWfsFeatures"
         @sendLayerInfo="toggleLayerInfo"
         @sendIsVisible="toggleTimeRange"
-      ></StaControl>
+      ></VectorControl>
 
       <WmsTimeControl v-if="mainLayer && !showPrintDialog"
           label="Layer 1"
@@ -342,15 +352,21 @@
       ></WmsTimeControl>
     </div>
 
-    <div v-if="staLayer && showStaLayer">
-      <StaLayer ref="staLayer"
+    <div v-if="staLayer && showVectorLayer">
+      <VectorLayer ref="staLayer"
           :layer="staLayer"
-          :sta-obs-property="staObsProperty"
-          :sta-filter-params="staInput"
-          :bounding-box="currentBoundingBox"
-          @sendLoadingStatus="setVectorDataLoading"
-          @sendFeatureInformation="setStaThing"
-      ></StaLayer>
+          @sendFeatureInformation="requestVectorData"
+          @zoomCluster="setCenterToCluster"
+      ></VectorLayer>
+    </div>
+
+
+    <div v-if="wfsLayer && showVectorLayer">
+      <VectorLayer
+          :layer="wfsLayer"
+          @sendFeatureInformation="requestVectorData"
+          @zoomCluster="setCenterToCluster"
+      ></VectorLayer>
     </div>
 
     <GeojsonLayer
@@ -415,18 +431,13 @@
       <MapPopup
         :clicked-coord="clickedCoord"
         :clicked-coord-wgs84="clickedCoordWgs84"
-        :single-result="dataCollection.mapPopupResult"
-        :sta-thing="staThing"
-        :popup-infos="popupInfos"
-        :selectable-sta-properties="thingProperties"
-        :selected-sta-property="staObsProperty"
         :show-time-series-button="showTimeSeriesButton"
-        :show-station-information="showStationInformation"
+        :single-result="timeseriesStore.dataCollection.mapPopupResult"
         v-show="isTablet || (!isTablet && !showSideNav)"
         v-if="showPopup"
         @popupClosed="closeChartAndPopup"
         @timeSeriesButtonClicked="showChart=true"
-        @sendObsProperty="setStaObsProperty"
+        @sendProperty="requestVectorData"
       ></MapPopup>
 
       <StickyPopup
@@ -464,9 +475,9 @@
       ></AggregationWidget>
 
       <TimeSeriesChart
-        :data-collection="dataCollection"
+        :data-collection="timeseriesStore.dataCollection"
         :clicked-coord-wgs84="clickedCoordWgs84"
-        :show-chart="showChart"
+        :show-chart="showChart && timeseriesStore.dataCollection.timeSeriesCollection.length > 0"
         :print-annotation="projectPrintAnnotation"
         @chartClosed="showChart = false"
         v-show="showChart && !showMainFilter && !showComparisonFilter"
@@ -484,7 +495,7 @@
     <div class="modal-content">
       <article class="message m-2">
 
-        <div class="message-body is-size-7 is-main-color">
+        <div class="message-body is-size-7 text-window">
 
           <div v-if="showAreaInfoBox" class="mb-3">
             <span class="has-text-weight-bold is-size-6">{{ $t("message.area") }}: {{ activeArea?.name }}</span>
@@ -614,19 +625,14 @@
 <style>
 
 #map {
-  height: 94vh;
-  margin-top: var(--header-height);
-  /*background-color: #837d7d;
-  background-image:  linear-gradient(#ababab 1px, transparent 1px), linear-gradient(to right, #ababab 1px, #837d7d 1px);
-  background-size: 54px 54px;*/
-  /* limit max-size to prevent too much load for geoserver */
-  /*max-width: 1600px;*/
-  /*max-height: 900px;*/
-  /*margin: auto;*/
+  height: calc(100vh - var(--header-offset)) ;
+  margin-top: var(--header-offset);
 }
 
 #sidenav, #sideNavModal {
-  height: 94vh;
+  height: calc(100vh - var(--header-height));
+  margin-top: var(--controls-offset);
+
   position: absolute;
   display: none;
 
@@ -664,7 +670,7 @@
 .tool-block {
   margin: auto;
   margin-top: -4px;
-  background: var(--ol-subtle-foreground-color);
+  background: var(--third-background-color);
   width: 258px;
 }
 
@@ -730,8 +736,8 @@
 
   border: 4px solid rgba(10, 10, 10, 0.1);
   border-radius: 50%;
-  border-top: 4px solid var(--main-color);
-  border-bottom: 4px solid var(--main-color);
+  border-top: 4px solid var(--active-background-color);
+  border-bottom: 4px solid var(--active-background-color);
 
   animation: spin 2s linear infinite;
 }
@@ -754,8 +760,8 @@
 }
 
 button.is-active, a.is-active, #nav-midsize-button.is-active, .active-caret {
-  background: var(--main-color) !important;
-  color: white !important;
+  background: var(--active-background-color) !important;
+  color: var(--header-text-highlight-color) !important;
 }
 
 .modal-content {
@@ -768,6 +774,15 @@ button.is-active, a.is-active, #nav-midsize-button.is-active, .active-caret {
   opacity: 0.5;
 }
 
+
+.ol-zoom {
+  top: calc(.5em + var(--controls-offset)) !important;
+}
+
+.ol-zoom-extent {
+  top: calc(4.643em + var(--controls-offset)) !important;
+}
+
 @media only screen and (min-width: 1408px) {
   #burgerButton {
     display: none;
@@ -805,9 +820,10 @@ button.is-active, a.is-active, #nav-midsize-button.is-active, .active-caret {
     cursor: default;
 }
 
-.is-main-color {
-    background: white;
-    border-color: var(--main-color) !important;
+.text-window {
+    color: var(--main-text-color) !important;
+    background: var(--second-background-color);
+    border-color: var(--first-background-color) !important;
     margin-left: -1px;
 }
 
@@ -905,8 +921,13 @@ button.is-active, a.is-active, #nav-midsize-button.is-active, .active-caret {
   line-height: 50px;
   vertical-align: middle !important;
   font-weight: bold;
-  color: white;
+  color: var(--header-text-highlight-color);
   font-size: 12px;
 }
 
+.standard-button {
+  background: var(--second-background-color) !important;
+  color: var(--main-text-color) !important;
+}
+
 </style>
diff --git a/frontend/src/components/map/MapLegend.vue b/frontend/src/components/map/MapLegend.vue
index 542aa8cbccf1dd5a453468fddffc9886037c7a03..1603d73f62d5e73829e9fe3e08e7b61739c4f157 100644
--- a/frontend/src/components/map/MapLegend.vue
+++ b/frontend/src/components/map/MapLegend.vue
@@ -1,5 +1,5 @@
 <template>
-  <div :id="legendId" :class="[showPrintLegend ? 'print-legend' : 'legend' ]" style="background: white">
+  <div :id="legendId" :class="[showPrintLegend ? 'print-legend' : 'legend' ]">
     <div class="legendContent">
 
       <font-awesome-icon v-if="!showPrintLegend" class="closeButton" :icon="['fas', 'rectangle-xmark']" size="lg" @click="toggleLegend" v-show="showLegend" />
@@ -51,7 +51,7 @@
 
         <button
             v-if="!showPrintLegend"
-            class="button is-small mt-1 infoButton"
+            class="button is-small mt-1 infoButton standard-button"
             @click="showLayerInfo"
             v-show="layerInfo"
         >
@@ -61,7 +61,7 @@
 
       <div
             v-show="!showLegend"
-            class="button is-size-7 unfoldLegend"
+            class="button is-size-7 unfoldLegend standard-button"
             @click="toggleLegend"
       >
           {{ $t("message.legend") }}
@@ -191,6 +191,9 @@ export default defineComponent({
     text-align: left;
     line-height: 0.5;
     border: 1px solid lightgray;
+
+    background: var(--second-background-color);
+    color: var(--main-text-color);
   }
 
   .legendColorList {
@@ -246,7 +249,7 @@ export default defineComponent({
     margin-right: 5px;
     border: 1px solid #666666;
     border-radius: 2px;
-    background: white;
+    background: var(--second-background-color);
   }
 
 </style>
\ No newline at end of file
diff --git a/frontend/src/components/map/MapPopup.vue b/frontend/src/components/map/MapPopup.vue
index 11c6e04999462a4f9f182b068393d80d94937a51..d94b46155166a6d4def1c84266d1dfb86b1604ee 100644
--- a/frontend/src/components/map/MapPopup.vue
+++ b/frontend/src/components/map/MapPopup.vue
@@ -12,21 +12,35 @@
 
         <p v-if="clickedCoordWgs84">Lat: {{ round4(clickedCoordWgs84[1]) }} / Lon: {{ round4(clickedCoordWgs84[0]) }}</p>
 
-        <div v-if="staThing && showStationInformation">
-          <p><b>{{ staThing.name}}</b></p>
-          <p
-              v-for="(popupInfo, index) in popupInfos"
-              :key="'thingProp' + index"
-          >
-            <i>{{ popupInfo.label }}: {{ popupInfo.text }}</i>
-          </p>
-
-          <ul id="thingPropList" class="mt-3 standardScrollbar">
+        <div v-if="timeseriesStore.staLayer || timeseriesStore.wfsLayer">
+
+          <div v-if="timeseriesStore.staLayer && timeseriesStore.staThing">
+            <p><b>{{ timeseriesStore.staThing.properties.name}}</b></p>
+            <p
+                v-for="(popupInfo, index) in timeseriesStore.popupInfos"
+                :key="'thingProp' + index"
+            >
+              <i>{{ popupInfo.label }}: {{ popupInfo.text }}</i>
+            </p>
+          </div>
+
+          <div v-if="timeseriesStore.wfsLayer">
+
+          <h2 class="m-2"><b>{{ locationName }}</b></h2>
+            <div v-if="timeseriesStore.feature">
+              <p v-for="info of wfsLocationInfos">
+                {{ info }}
+              </p>
+            </div>
+          </div>
+
+
+          <ul id="locationPropertyList" class="mt-3 standardScrollbar">
             <li
-                class="staProperty"
-                :class="[selectedStaProperty?.id == property.id ? 'activeStaProperty' : '']"
-                v-for="property of selectableStaProperties"
-                :key="'obs_prop' + property.id"
+                class="locationProperty"
+                :class="[timeseriesStore.selectedProperty?.id == property.id ? 'activeProperty' : '']"
+                v-for="property of timeseriesStore.locationProperties"
+                :key="'loc_prop' + property.id"
                 @click="switchProperty(property)"
             >
               {{ property.fullName }}
@@ -34,7 +48,7 @@
           </ul>
         </div>
 
-        <div v-if="singleResult && !showStationInformation">
+        <div v-if="singleResult && !(featureStore.staLayer || featureStore.wfsLayer)">
           <p class="has-text-weight-bold mt-1">{{ singleResult.label }}</p>
           <p class="has-text-weight-bold mt-1" v-show="singleResult.time !== ''">{{ singleResult.time }}</p>
 
@@ -51,14 +65,12 @@
 
                 <span v-else>{{ singleResult.textValue }}</span>
             </p>
-
           </div>
         </div>
 
         <button v-show="showTimeSeriesButton" class="button mt-1 is-small" @click="showTimeSeries">
           {{ $t("message.timeSeries") }}
         </button>
-
     </div>
   </div>
 </ol-overlay>
@@ -69,20 +81,55 @@
 
 import { defineComponent, PropType } from "vue";
 import { round4 } from "@/composables/utils";
-import { ObservedProperty, StaPopupInfo, StaThing } from "@/types/sta";
-import { Coordinate, CovJsonSingleResult } from "@/types";
+import {Coordinate, CovJsonSingleResult} from "@/types";
+import {TimeseriesStore, useTimeseriesStore} from "@/stores/timeseries.ts";
+import {FeatureStore, useFeatureStore} from "@/stores/feature.ts";
+import {VectorProperty} from "@/types/vector";
 
 export default defineComponent({
   props: {
     clickedCoord: Object as PropType<Coordinate>,
     clickedCoordWgs84: Object as PropType<Coordinate>,
     singleResult: Object as PropType<CovJsonSingleResult>,
-    staThing: Object as PropType<StaThing|undefined>,
-    popupInfos: Array as Object as PropType<StaPopupInfo[]>,
-    selectableStaProperties: Array as Object as PropType<ObservedProperty[]>,
-    selectedStaProperty: Object as PropType<ObservedProperty|undefined>,
-    showTimeSeriesButton: Boolean,
-    showStationInformation: Boolean
+    showTimeSeriesButton: Boolean
+  },
+  data() {
+    return {
+      timeseriesStore: {} as TimeseriesStore,
+      featureStore: {} as FeatureStore
+    }
+  },
+  computed: {
+    locationName() {
+      if (this.timeseriesStore.feature) {
+        if (this.timeseriesStore.feature.properties) {
+          return this.timeseriesStore.feature.properties['name']
+        }
+      }
+      return ''
+    },
+    wfsLocationInfos() {
+      const result = []
+      if (this.timeseriesStore.feature) {
+        if (this.timeseriesStore.feature.properties) {
+          for (const key of Object.keys(this.timeseriesStore.feature.properties)) {
+            if (['name', 'created_at', 'import_id', 'type', 'srid', 'lat', 'lon', 'gtype'].indexOf(key) !== -1)
+              continue
+
+            if (key === 'properties') {
+              const props = JSON.parse(this.timeseriesStore.feature.properties[key])
+              for (const subKey of Object.keys(props)) {
+                result.push(subKey + ': ' + props[subKey])
+              }
+            } else {
+              result.push(key + ': ' + this.timeseriesStore.feature.properties[key])
+            }
+          }
+        }
+      }
+
+      return result
+    }
   },
   methods: {
     close() {
@@ -94,20 +141,30 @@ export default defineComponent({
     showTimeSeries() {
       this.$emit('timeSeriesButtonClicked')
     },
-    switchProperty(property: ObservedProperty|undefined) {
-      this.$emit('sendObsProperty', property)
+    switchProperty(property: VectorProperty) {
+      this.timeseriesStore.isClickOnFeature = true
+      this.timeseriesStore.setSelectedProperty(property)
+      this.featureStore.setSelectedProperty(property)
+
+      this.timeseriesStore.requestPointData()
+
+      this.$emit('sendProperty')
     }
   },
   emits: {
     popupClosed() {
-      return true;
+      return true
     },
-    sendObsProperty(){
-        return true;
+    sendProperty(){
+      return true
     },
     timeSeriesButtonClicked() {
-      return true;
+      return true
     }
+  },
+  created() {
+    this.timeseriesStore = useTimeseriesStore()
+    this.featureStore = useFeatureStore()
   }
 })
 </script>
@@ -116,7 +173,8 @@ export default defineComponent({
 <style>
 
 #popup {
-  background: white;
+  background: var(--second-background-color);
+  color: var(--main-text-color);
   border: 1px solid #cccccc;
   border-radius: 4px;
   margin-left: -24px;
@@ -143,28 +201,28 @@ export default defineComponent({
 }
 
 #popupArrow::after {
-  border-color: #fff transparent;
+  border-color: var(--second-background-color) transparent;
   top: -3px;
   left: -1px;
 }
 
-#thingPropList {
+#locationPropertyList {
   max-height: 240px;
 }
 
-.activeStaProperty {
+.activeProperty {
   color: red;
   font-weight: bold;
 }
 
-.staProperty {
+.locationProperty {
   white-space: nowrap;
   overflow-x: hidden;
   text-overflow: ellipsis;
 }
 
-.staProperty:hover {
-  color: var(--main-color);
+.locationProperty:hover {
+  color: var(--first-background-color);
   font-weight: bolder;
   cursor: pointer;
 }
diff --git a/frontend/src/components/map/MapPrint.vue b/frontend/src/components/map/MapPrint.vue
index 1d42d9fa270efe8059a3ed607700977d80cf65f3..74158e639698be6890d1958e7983a72de4f0e686 100644
--- a/frontend/src/components/map/MapPrint.vue
+++ b/frontend/src/components/map/MapPrint.vue
@@ -36,8 +36,8 @@
                     <input type="radio" id="radioOptionCurrent" value="map" v-model="extent"/>
                     <label class="radio is-size-7 m-1" for="radioOptionCurrent">{{ $t("message.extentCurrent") }}</label>
                     <br>
-                    <button class="button is-small" @click="print">{{ $t("message.save") }}</button>
-                    <button class="button is-small" @click="printLegend">{{ $t("message.printLegend") }}</button>
+                    <button class="button is-small standard-button" @click="print">{{ $t("message.save") }}</button>
+                    <button class="button is-small standard-button" @click="printLegend">{{ $t("message.printLegend") }}</button>
 
                     <div class="mt-2">
                       <div id="print-loading-ring" v-show="isDownloading"></div>
@@ -321,8 +321,8 @@ export default defineComponent({
 
   border: 4px solid rgba(10, 10, 10, 0.1);
   border-radius: 50%;
-  border-top: 4px solid var(--main-color);
-  border-bottom: 4px solid var(--main-color);
+  border-top: 4px solid var(--active-background-color);
+  border-bottom: 4px solid var(--active-background-color);
 
   animation: spin 2s linear infinite;
 }
diff --git a/frontend/src/components/map/MapPrintControl.vue b/frontend/src/components/map/MapPrintControl.vue
index 742000839159535843b187a0099de75122b8b68f..71fcb9275a8d8ffd555a8c29a29f165445e755bb 100644
--- a/frontend/src/components/map/MapPrintControl.vue
+++ b/frontend/src/components/map/MapPrintControl.vue
@@ -39,7 +39,7 @@ export default defineComponent({
 
 #mapPrintControl {
   right: .5em;
-  top: 4.5em;
+  top: calc(4.5em + var(--controls-offset));
   pointer-events: auto;
 }
 
diff --git a/frontend/src/components/map/ProjectTeaser.vue b/frontend/src/components/map/ProjectTeaser.vue
index 846bc2b9477bb37f51ca90db03f27d8c3c4d37e7..9a561f56aef140551093df5afb9a1936d4dee202 100644
--- a/frontend/src/components/map/ProjectTeaser.vue
+++ b/frontend/src/components/map/ProjectTeaser.vue
@@ -37,9 +37,9 @@ export default defineComponent({
   #teaser {
     width: 720px;
 
-    background-color: var(--main-color);
+    background-color: var(--first-background-color);
     opacity: 0.7;
-    color: var(--ol-subtle-foreground-color);
+    color: var(--header-text-color);
     border-radius: 4px;
 
     top: calc(var(--header-height) + 10px);
diff --git a/frontend/src/components/map/TimeSeriesChart.vue b/frontend/src/components/map/TimeSeriesChart.vue
index 6cfcfbbb2c3563f88d2f276d541abf9af62ed6ae..4ff08132100fa1dc5f5664d39af83c6241b94474 100644
--- a/frontend/src/components/map/TimeSeriesChart.vue
+++ b/frontend/src/components/map/TimeSeriesChart.vue
@@ -28,7 +28,7 @@
           </p>
         </div>
 
-        <div v-show="showTable">
+        <div id="resultTable" v-show="showTable">
           <div class="has-text-right">
             <font-awesome-icon class="tableButton tableButtonCSV" :icon="['fas', 'file-csv']" @click="exportCSV" :title="$t('message.exportCSV')" />
             <font-awesome-icon class="tableButton tableButtonToggle" :icon="['fas', 'chart-line']" @click="toggleTable" :title="$t('message.showDiagram')" />
@@ -144,7 +144,7 @@ export default defineComponent({
         traces: [
           {
             line: {
-              color: "#00589C",
+              color: getComputedStyle(document.documentElement).getPropertyValue('--main-text-color'),
               width: 4,
               shape: "line"
             }
@@ -159,7 +159,7 @@ export default defineComponent({
             y: chartMarginBottom
           },
           yaxis: {
-            linecolor: '#cccccc',
+            linecolor: getComputedStyle(document.documentElement).getPropertyValue('--outline-color'),
             mirror: true,
             fixedrange: false,
             automargin: true,
@@ -167,12 +167,11 @@ export default defineComponent({
             showticksuffix: 'none',
             hoverformat: '.2f'
           } as any,
-          //paper_bgcolor: 'lightblue',
           title: {
             text: '',
             font: {
               size: 12,
-              color: '#4a4a4a'
+              color: getComputedStyle(document.documentElement).getPropertyValue('--main-text-color')
             }
           },
           annotations: [],
@@ -327,7 +326,7 @@ export default defineComponent({
         text: this.lat + ' - ' + this.lon,
         font: {
           size: 12,
-          color: '#4a4a4a'
+          color: getComputedStyle(document.documentElement).getPropertyValue('--main-text-color')
         }
       }
       if (!this.isWideScreen) {
@@ -422,8 +421,12 @@ export default defineComponent({
       // use timeout because plotly cannot react to changed width of bulma modal-content
       setTimeout(function () {
         const element = document.getElementById(plotlyId)
-        if (element) {
-          Plotly.relayout(plotlyId, {width: element.offsetWidth});
+        if (element && this.timeSeriesCollection) {
+          Plotly.relayout(plotlyId, {
+            paper_bgcolor: getComputedStyle(document.documentElement).getPropertyValue('--second-background-color'),
+            plot_bgcolor: getComputedStyle(document.documentElement).getPropertyValue('--second-background-color'),
+            width: element.offsetWidth
+          });
         }
       }, 10)
     },
@@ -492,7 +495,8 @@ export default defineComponent({
   overflow-y: scroll;
   scrollbar-width: none;
   padding: 12px;
-  border: 1px solid #cccccc;
+  border: 1px solid var(--outline-color);
+  background: var(--second-background-color);
 }
 
 #covJsonWidgetContent::-webkit-scrollbar {
@@ -551,8 +555,8 @@ export default defineComponent({
 }
 
 .tabs li.is-active a {
-  border-bottom-color: var(--main-color);
-  color: var(--main-color);
+  border-bottom-color: var(--main-text-color);
+  color: var(--main-text-color);
 }
 
 .timeseriesTable {
@@ -564,4 +568,8 @@ export default defineComponent({
   overflow-y: hidden;
 }
 
+#resultTable {
+  color: var(--main-text-color);
+}
+
 </style>
\ No newline at end of file
diff --git a/frontend/src/components/map/sta/StaLayer.vue b/frontend/src/components/map/sta/StaLayer.vue
index 8d935f825f3014e06803278d9d10893c6c4fece3..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 100644
--- a/frontend/src/components/map/sta/StaLayer.vue
+++ b/frontend/src/components/map/sta/StaLayer.vue
@@ -1,457 +0,0 @@
-<template>
-
-	<ol-interaction-select
-    @select="featureSelected"
-		:condition="selectConditionClick"
-		:filter="selectInteractionFilterClick"
-    v-if="!showCluster"
-	>
-    <ol-style :overrideStyleFunction="getSelectStyle">
-    </ol-style>
-	</ol-interaction-select>
-
-	<ol-interaction-select
-    @select="featureSelected"
-		:condition="selectConditionHover"
-		:filter="selectInteractionFilterHover"
-    v-if="!showCluster"
-	>
-
-		<ol-style :overrideStyleFunction="getHoverStyle">
-		</ol-style>
-	</ol-interaction-select>
-
-	<ol-vector-image-layer :z-index="30">
-		<ol-source-vector
-      @featuresloadstart="$emit('sendLoadingStatus', true)"
-      @featuresloaderror="$emit('sendLoadingStatus', false)"
-      @featuresloadend="$emit('sendLoadingStatus', false)"
-    >
-      <ol-feature
-          v-for="(point, i) in locations"
-          :key="'point' + i"
-          :properties="point.properties"
-          v-if="!showCluster"
-      >
-        <ol-geom-point :coordinates="point.coordinates"></ol-geom-point>
-      </ol-feature>
-
-      <ol-feature
-          v-for="(cluster, i) in clusters"
-          :key="'cluster' + i"
-          v-if="showCluster"
-      >
-        <ol-geom-point :coordinates="cluster.coords" v-if="cluster.requestId === currentRequestId"></ol-geom-point>
-        <ol-style>
-          <ol-style-circle :radius="cluster.count / 50 + 30">
-            <ol-style-stroke color="#00589C" :width="5"></ol-style-stroke>
-            <ol-style-fill color="#00589C1A"></ol-style-fill>
-
-          </ol-style-circle>
-          <ol-style-text :text="'' + cluster.count" scale="2">
-            <ol-style-stroke color="white" :width="6"></ol-style-stroke>
-            <ol-style-fill color="#333"></ol-style-fill>
-          </ol-style-text>
-        </ol-style>
-      </ol-feature>
-
-      <ol-style :overrideStyleFunction="getNormalStyle">
-      </ol-style>
-		</ol-source-vector>
-
-  </ol-vector-image-layer>
-
-</template>
-
-<script lang="ts">
-import {defineComponent, inject, PropType} from "vue";
-import axios from "axios";
-import Circle from "ol/style/Circle";
-import Fill from "ol/style/Fill";
-import Stroke from "ol/style/Stroke";
-import Style from "ol/style/Style";
-import proj4 from "proj4";
-import {Cluster, MatrixCell, ObservedProperty, STAFilterParams, STALayer, StaThing} from "@/types/sta";
-import {BoundingBox} from "@/types";
-
-const MAX_LOCATIONS_ON_DESKTOP = 300
-const MAX_LOCATIONS_ON_MOBILE = 200
-
-const MATRIX_WIDTH = 5
-const MATRIX_HEIGHT = 5
-const MATRIX: MatrixCell[] = [
-    [[0,0],[1,1], 1],
-    [[0,1],[1,2], 2],
-    [[0,2],[1,3], 3],
-    [[0,3],[1,4], 4],
-    [[0,4],[1,5], 5],
-
-    [[1,0],[2,1], 6],
-    [[1,1],[2,2], 7],
-    [[1,2],[2,3], 8],
-    [[1,3],[2,4], 9],
-    [[1,4],[2,5], 10],
-
-    [[2,0],[3,1], 11],
-    [[2,1],[3,2], 12],
-    [[2,2],[3,3], 13],
-    [[2,3],[3,4], 14],
-    [[2,4],[3,5], 15],
-
-    [[3,0],[4,1], 16],
-    [[3,1],[4,2], 17],
-    [[3,2],[4,3], 18],
-    [[3,3],[4,4], 19],
-    [[3,4],[4,5], 20],
-
-    [[4,0],[5,1], 21],
-    [[4,1],[5,2], 22],
-    [[4,2],[5,3], 23],
-    [[4,3],[5,4], 24],
-    [[4,4],[5,5], 25],
-]
-
-
-export default defineComponent({
-	props: {
-    layer: Object as PropType<STALayer>,
-    staObsProperty: Object as PropType<ObservedProperty|undefined>,
-    staFilterParams: Object as PropType<STAFilterParams>,
-    boundingBox: Object as PropType<BoundingBox>
-	},
-  data() {
-    return {
-      dataLayer: this.$props.layer as STALayer,
-      locations: [] as any[],
-      thresholdValue: '' as string,
-      beginDate: '' as string,
-      endDate: '' as string,
-      currentRequestId: 0 as number,
-      locationsCount: 0 as number,
-      absoluteLocationsCount: 0 as number,
-      clusters: [] as Cluster[]
-    }
-  },
-  computed: {
-    thingCountUrl() {
-      return import.meta.env.VITE_APP_BACKEND_URL + 'sta-thing-count/' + this.dataLayer.own_id
-    },
-    locationsUrl() {
-      return import.meta.env.VITE_APP_BACKEND_URL + 'sta-locations/' + this.dataLayer.own_id
-    },
-    endpoint() {
-      return this.$props.layer.endpoint
-    },
-    obsPropertyId() {
-      if (this.$props.staObsProperty) {
-        return this.$props.staObsProperty.id
-      }
-      return undefined
-    },
-    bboxTopLon() {
-      return this.$props.boundingBox[0][0]
-    },
-    bboxTopLat() {
-      return this.$props.boundingBox[0][1]
-    },
-    bboxBottomLon() {
-      return this.$props.boundingBox[1][0]
-    },
-    bboxBottomLat() {
-      return this.$props.boundingBox[1][1]
-    },
-    showCluster() {
-      return this.clusters.length > 0
-    },
-    cellWidth() {
-      return Math.abs((this.bboxTopLon - this.bboxBottomLon) / MATRIX_WIDTH)
-    },
-    cellHeight() {
-      return Math.abs((this.bboxTopLat - this.bboxBottomLat) / MATRIX_HEIGHT)
-    },
-    maxLocations() {
-      return (window.innerWidth > 1408) ? MAX_LOCATIONS_ON_DESKTOP : MAX_LOCATIONS_ON_MOBILE
-    }
-  },
-  watch: {
-      staObsProperty() {
-        this.absoluteLocationsCount = 0
-        this.requestLocations()
-      },
-      staFilterParams(val: STAFilterParams) {
-        this.absoluteLocationsCount = 0
-        this.thresholdValue = val.threshold
-        this.beginDate = val.beginDate
-        this.endDate = val.endDate
-        this.requestLocations()
-      },
-      boundingBox() {
-        this.requestLocations()
-      }
-  },
-  methods: {
-    getFilterParams(cell: MatrixCell|undefined = undefined) {
-      const params = []
-
-      if (this.obsPropertyId) {
-        params.push('obsProperty=' + this.obsPropertyId)
-      }
-      if (this.thresholdValue !== '') {
-        params.push('threshold=' + this.thresholdValue)
-      }
-      if (this.beginDate !== '') {
-        params.push('beginDate=' + this.beginDate)
-      }
-      if (this.endDate !== '') {
-        params.push('endDate=' + this.endDate)
-      }
-
-      if (cell) {
-        const cellTopLon = this.bboxTopLon + (this.cellWidth * cell[0][0])
-        const cellTopLat = this.bboxTopLat - (this.cellHeight * cell[0][1])
-
-        const cellBottomLon = this.bboxTopLon + (this.cellWidth * cell[1][0])
-        const cellBottomLat = this.bboxTopLat - (this.cellHeight * cell[1][1])
-
-        params.push('bboxTopLon=' + cellTopLon)
-        params.push('bboxTopLat=' + cellTopLat)
-        params.push('bboxBottomLon=' + cellBottomLon)
-        params.push('bboxBottomLat=' + cellBottomLat)
-
-      } else {
-
-        params.push('bboxTopLon=' + this.bboxTopLon)
-        params.push('bboxTopLat=' + this.bboxTopLat)
-        params.push('bboxBottomLon=' + this.bboxBottomLon)
-        params.push('bboxBottomLat=' + this.bboxBottomLat)
-      }
-
-      if (params.length > 0) {
-        return '?' + params.join('&')
-      }
-      return ''
-    },
-    requestLocations() {
-
-      axios.get(this.thingCountUrl + this.getFilterParams())
-        .then(response => {
-
-          this.locationsCount = response.data["@iot.count"]
-
-          if (this.locationsCount < this.absoluteLocationsCount || this.absoluteLocationsCount === 0) {
-
-            this.clusters = []
-            this.locations = []
-            this.$emit('sendLoadingStatus', true)
-
-            if (this.locationsCount > this.maxLocations || this.locationsCount === 0) {
-              this.requestCluster()
-            } else {
-              this.requestFullLocations()
-            }
-          }
-        })
-        .catch((error: any) => {
-            console.log(error)
-        })
-        .finally(() => {
-          if (this.absoluteLocationsCount < this.locationsCount) {
-            this.absoluteLocationsCount = this.locationsCount
-          }
-        })
-    },
-    requestCluster() {
-
-      const requestId = Math.floor(Math.random() * 1000000)
-      this.currentRequestId = requestId
-
-      let countFinishedRequests = 0
-
-      const minDistX = this.cellWidth / 4
-      const minDistY = this.cellHeight / 4
-
-      for (const cell of MATRIX) {
-
-        axios.get(this.locationsUrl + this.getFilterParams(cell))
-          .then(response => {
-
-            const countFeatures = response.data["@iot.count"]
-            if (countFeatures > 0 && requestId === this.currentRequestId) {
-
-              const idx = Math.floor(countFeatures / 2)
-
-              if (response.data.features[idx]) {
-
-                const newCluster = {
-                  coords: proj4( 'WGS84', 'EPSG:3857', response.data.features[idx].geometry.coordinates),
-                  coordsWgs84: response.data.features[idx].geometry.coordinates,
-                  count: countFeatures,
-                  requestId: requestId,
-                  cellNumber: cell[2]
-                }
-
-                let isClusterTooNearToOtherOne = false
-
-                for (const cluster of this.clusters) {
-                  const distX = Math.abs(Math.abs(cluster.coordsWgs84[0]) - Math.abs(newCluster.coordsWgs84[0]))
-                  const distY = Math.abs(Math.abs(cluster.coordsWgs84[1]) - Math.abs(newCluster.coordsWgs84[1]))
-
-                  if (distX < minDistX && distY < minDistY) {
-                    isClusterTooNearToOtherOne = true
-
-                    const midpoint = [(cluster.coordsWgs84[0] + newCluster.coordsWgs84[0]) / 2, (cluster.coordsWgs84[1] + newCluster.coordsWgs84[1]) / 2];
-
-                    cluster.coords = proj4( 'WGS84', 'EPSG:3857', midpoint)
-                    cluster.coordsWgs84 = midpoint
-                    cluster.count += newCluster.count
-                  }
-                }
-                if (!isClusterTooNearToOtherOne && requestId === this.currentRequestId) {
-                  this.clusters.push(newCluster)
-                }
-              }
-            }
-          })
-          .catch((error: any) => {
-            console.log(error)
-          })
-          .finally(() => {
-            countFinishedRequests++
-            if (countFinishedRequests === MATRIX.length) {
-              this.$emit('sendLoadingStatus', false)
-            }
-          })
-      }
-    },
-    requestFullLocations() {
-      axios.get(this.locationsUrl + this.getFilterParams())
-        .then(response => {
-          if (response.data['features']) {
-            for (const point of response.data['features']) {
-              this.locations.push({
-                coordinates: proj4( 'WGS84', 'EPSG:3857',point.geometry.coordinates),
-                properties: point.properties
-              })
-            }
-          }
-        })
-        .catch((error: any) => {
-          console.log(error)
-        })
-        .finally(() => {
-          this.$emit('sendLoadingStatus', false)
-        })
-    },
-    selectInteractionFilterClick(feature: any) {
-
-      let thing = feature.values_
-
-      if (thing.id !== undefined) {
-
-        const url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-thing/' + this.dataLayer.endpoint + '/' + thing.id
-
-        axios.get(url)
-            .then(response => {
-              thing = response.data.value[0]
-            })
-            .catch((error: any) => {
-                console.log(error)
-            })
-            .finally(() => {
-              this.$emit('sendFeatureInformation', thing)
-            })
-      }
-
-      return true
-    },
-    getStyleCircle(feature: any, borderWidth: number, borderColor: string) {
-
-      const lastObservationResult = feature.values_['Datastreams/0/Observations/0/result']
-
-      let radius = 10
-      for (const staColor of this.dataLayer.legend) {
-          if (lastObservationResult < staColor.upper_limit) {
-            radius = staColor.radius
-
-            if (staColor.color != '#FFFFFF') {
-              borderColor = staColor.color
-            }
-          }
-      }
-
-      feature.setStyle(new Style({
-          image: new Circle({
-            radius: radius,
-            fill: new Fill({
-              color: (borderColor == 'red') ? 'white' : '#ffffff6e'
-            }),
-            stroke: new Stroke({
-              color: borderColor,
-              width: borderWidth
-            })
-          })
-      }))
-    },
-    getNormalStyle(feature: any) {
-      this.getStyleCircle(feature, 4, '#00589C')
-    },
-    getHoverStyle(feature: any) {
-      this.getStyleCircle(feature, 8, 'red')
-    },
-    getSelectStyle(feature: any) {
-      this.getStyleCircle(feature, 8, 'red')
-    }
-  },
-  emits: {
-    sendLoadingStatus(isLoading: boolean) {
-      if (isLoading || !isLoading) {
-        return true
-      } else {
-        console.warn('Invalid submit event payload!')
-        return false
-      }
-    },
-    sendFeatureInformation(id: StaThing) {
-      if (id) {
-        return true
-      } else {
-        console.warn('Invalid submit event payload!')
-        return false
-      }
-    }
-  },
-  created() {
-    this.dataLayer = this.$props.layer
-
-    this.requestLocations()
-  },
-  setup() {
-		const strategy: any = inject("ol-loadingstrategy");
-		const bbox = strategy.bbox;
-
-		const format: any = inject("ol-format");
-		const GeoJSON = new format.GeoJSON();
-
-		const selectConditions: any = inject("ol-selectconditions")
-		const selectConditionClick = selectConditions.click
-
-		const selectConditionHover = selectConditions.pointerMove
-
-    const featureSelected = () => {}
-
-    const selectInteractionFilterHover = () => {
-      return true
-    }
-
-		return {
-      bbox,
-			GeoJSON,
-			selectConditionClick,
-			selectConditionHover,
-			featureSelected,
-      selectInteractionFilterHover
-		};
-	},
-});
-
-</script>
diff --git a/frontend/src/components/map/sta/StaControl.vue b/frontend/src/components/map/vector/VectorControl.vue
similarity index 50%
rename from frontend/src/components/map/sta/StaControl.vue
rename to frontend/src/components/map/vector/VectorControl.vue
index 0cea1b58a3978b06b44b8c36088e9037a8cf58e1..3aea3ac05a0f906a039edbbb0ad0ded33f8298bf 100644
--- a/frontend/src/components/map/sta/StaControl.vue
+++ b/frontend/src/components/map/vector/VectorControl.vue
@@ -3,7 +3,7 @@
 
   <div class="bottomControlWrapper">
 
-  <div id="staControlBar" class="bottomControl">
+  <div id="vectorControlBar" class="bottomControl">
     <div class="is-size-7 columns">
 
       <div class="column" v-show="isWmsTimeControlActive">
@@ -18,30 +18,30 @@
       </div>
       <div class="column">
 
-        <p class="has-text-weight-bold">{{ $t("message.observations") }}</p>
+        <p class="has-text-weight-bold">{{ $t("message.timeSeries") }}</p>
 
         <div class="dropdown is-up is-inline-block">
           <div class="dropdown-trigger" @click="togglePropertyMenu">
-            <input class="input is-small" type="text" v-model="staAutoComplete" :placeholder="propertyInputPlaceholder">
+            <input class="input is-small" type="text" v-model="autoComplete" :placeholder="timeseriesStore.inputPlaceholder">
           </div>
 
-          <div class="dropdown-menu" :class="[showPropertyMenu ? 'activePropertyMenu' : '']" id="dropdown-menu3" role="menu">
+          <div class="dropdown-menu" :class="[showPropertyMenu ? 'activePropertyMenu' : '']" role="menu">
             <div class="dropdown-content">
               <a
                 href="#"
                 @click="switchProperty(undefined)"
                 class="dropdown-item is-small"
-                :class="[selectedProperty === undefined ? 'is-active' : '']"
+                :class="[timeseriesStore.selectedProperty === undefined ? 'is-active' : '']"
               >
                 -
               </a>
 
               <a
                 href="#"
-                v-for="property in selectableProperties" :key="property.id"
+                v-for="property in timeseriesStore.layerProperties" :key="property.id"
                 @click="switchProperty(property)"
                 class="dropdown-item is-small"
-                :class="[property.id === selectedProperty?.id && selectedProperty?.id ? 'is-active' : '']"
+                :class="[property.id == timeseriesStore.selectedProperty?.id ? 'is-active' : '']"
                 v-show="searchByAutoCompleteInput(property)"
               >
                 {{ property.fullName }}
@@ -55,18 +55,20 @@
         <p class="has-text-weight-bold">{{ $t("message.measurementsFrom") }}:</p>
         <p class="has-text-danger" v-show="showBeginDateWarning">{{ $t("message.invalidDate") }}</p>
 
-        <input class="input is-small" type="text" v-model="staBeginDate" placeholder="YYYY-MM-DD" @keyup.enter="setFilter">
+        <input class="input is-small" type="text" v-model="beginDate" placeholder="YYYY-MM-DD" @keyup.enter="setFilter">
       </div>
 
       <div class="column" :class="[isWmsTimeControlActive ? 'is-one-fifth' : 'is-one-quarter']">
         <p class="has-text-weight-bold">{{ $t("message.measurementsUntil") }}:</p>
         <p class="has-text-danger" v-show="showEndDateWarning">{{ $t("message.invalidDate") }}</p>
-        <input class="input is-small" type="text" v-model="staEndDate" placeholder="YYYY-MM-DD" @keyup.enter="setFilter">
+        <input class="input is-small" type="text" v-model="endDate" placeholder="YYYY-MM-DD" @keyup.enter="setFilter">
       </div>
 
       <div class="column">
         <p class="has-text-weight-bold">{{ $t("message.threshold") }}:</p>
-        <input class="input is-small" type="text" v-model="staInput" placeholder="e.g 0.5" @keyup.enter="setFilter">
+        <input class="input is-small" type="text" v-model="thresholdValue" placeholder="e.g 0.5" @keyup.enter="setFilter">
+        <p class="has-text-danger" v-show="showPropertyWarning">{{ $t("message.invalidProperty") }}</p>
+        <p class="has-text-danger" v-show="showThresholdWarning">{{ $t("message.invalidThreshold") }}</p>
       </div>
 
       <div class="column">
@@ -82,7 +84,6 @@
         </button>
       </div>
 
-
     </div>
   </div>
   </div>
@@ -92,114 +93,109 @@
 <script lang="ts">
 
 import {defineComponent, PropType} from "vue";
-import axios from "axios";
 import {isValidDateFormat} from "@/composables/time_utils";
-import {ObservedProperty, STAFilterParams, STALayer} from "@/types/sta";
+import {FeatureStore, useFeatureStore} from "@/stores/feature.ts";
+import {TimeseriesStore, useTimeseriesStore} from "@/stores/timeseries.ts";
+import {Layer} from "@/types";
+import {VectorProperty} from "@/types/vector";
 
 export default defineComponent({
   props: {
-    layer: Object as PropType<STALayer>,
-    obsProperty: Object as PropType<ObservedProperty>,
+    layer: Object as PropType<Layer>,
     isWmsTimeControlActive: Boolean
   },
   data() {
     return {
-      selectableProperties: [] as ObservedProperty[],
       showPropertyMenu: false as boolean,
-      selectedProperty: {} as ObservedProperty|undefined,
-      staAutoComplete: '' as string,
-      staBeginDate: '' as string,
-      staEndDate: '' as string,
-      staInput: '' as string,
+      autoComplete: '' as string,
+      beginDate: '' as string,
+      endDate: '' as string,
+      thresholdValue: '' as string,
       showBeginDateWarning: false as boolean,
-      showEndDateWarning: false as boolean
-    }
-  },
-  computed: {
-    endpoint() {
-      return this.$props.layer.endpoint
-    },
-    propertyInputPlaceholder() {
-      return this.selectedProperty ? this.selectedProperty.fullName : '-'
+      showEndDateWarning: false as boolean,
+      showPropertyWarning: false as boolean,
+      showThresholdWarning: false as boolean,
+      featureStore: {} as FeatureStore,
+      timeseriesStore: {} as TimeseriesStore
     }
   },
   methods: {
     togglePropertyMenu(){
       this.showPropertyMenu = !this.showPropertyMenu
     },
-    getSelectableProperties() {
-      this.selectableProperties = []
-
-      let url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-obs-properties/' + this.layer.own_id
-
-      axios.get(url)
-          .then(response => {
-
-            const name_prop = (this.$i18n.locale == 'de') ? 'name_de' : 'name_en'
-            for (const property of response.data.value) {
-
-              let fullName = property.name
-              if (property.properties) {
-                if (property.properties[name_prop]) {
-                  fullName = property.properties[name_prop]
-                }
-              }
-              this.selectableProperties.push({
-                  id: property['@iot.id'],
-                  fullName: fullName
-              })
-            }
-            this.selectableProperties.sort((a: ObservedProperty, b: ObservedProperty) => a.fullName.localeCompare(b.fullName))
-          })
-          .catch((error: any) => {
-              console.log(error)
-          })
-    },
-    switchProperty(property: ObservedProperty|undefined) {
-      this.selectedProperty = property
+    switchProperty(property: VectorProperty|undefined) {
       this.showPropertyMenu = false
-      this.$emit('sendObsProperty', property)
-      this.staAutoComplete = ''
+      this.autoComplete = ''
+
+      if (property) {
+        this.featureStore.setSelectedProperty(property)
+        this.timeseriesStore.setSelectedProperty(property)
+        this.$emit('sendVectorFilter')
+
+        this.timeseriesStore.requestPointData()
+      } else {
+        this.clear()
+      }
     },
     setFilter() {
-      if (this.staBeginDate !== '' || this.staEndDate !== '' || this.staInput !== '') {
 
         this.showBeginDateWarning = false
         this.showEndDateWarning = false
-        if (!isValidDateFormat(this.staBeginDate)) {
+        this.showPropertyWarning = false
+        this.showThresholdWarning = false
+
+        if (!isValidDateFormat(this.beginDate)) {
           this.showBeginDateWarning = true
           return
         }
-        if (!isValidDateFormat(this.staEndDate)) {
+
+        if (!isValidDateFormat(this.endDate)) {
           this.showEndDateWarning = true
           return
         }
 
-        this.$emit('sendStaFilterParams', {
-          threshold: this.staInput,
-          beginDate: this.staBeginDate,
-          endDate: this.staEndDate
-        })
-      }
+        if (this.thresholdValue !== '') {
+          if (!this.featureStore.selectedPropertyId) {
+            this.showPropertyWarning = true
+            return
+          }
+
+          const floatVal = parseFloat(this.thresholdValue)
+          if (isNaN(floatVal)) {
+            this.showThresholdWarning = true
+            return
+          } else {
+            this.thresholdValue = floatVal
+          }
+        }
+
+        this.featureStore.beginDate = this.beginDate
+        this.featureStore.endDate = this.endDate
+        this.featureStore.thresholdValue = this.thresholdValue
+
+        this.$emit('sendVectorFilter')
     },
-    searchByAutoCompleteInput(property: ObservedProperty) {
-      const searchTerm = this.staAutoComplete.toLowerCase()
+    searchByAutoCompleteInput(property: VectorProperty) {
+      const searchTerm = this.autoComplete.toLowerCase()
       const keyword = property.fullName.toLowerCase()
       return (keyword.includes(searchTerm))
     },
     clear() {
-      this.selectedProperty = undefined
-      this.staBeginDate = ''
-      this.staEndDate = ''
-      this.staInput = ''
-      this.$emit('sendObsProperty', undefined)
-      this.$emit('sendStaFilterParams', {
-        threshold: '',
-        beginDate: '',
-        endDate: ''
-      })
+      this.featureStore.setSelectedProperty(undefined)
+      this.timeseriesStore.setSelectedProperty(undefined)
+      this.timeseriesStore.unsetTimeseriesData()
+
       this.showBeginDateWarning = false
       this.showEndDateWarning = false
+
+      this.beginDate = ''
+      this.endDate = ''
+      this.thresholdValue = ''
+      this.featureStore.beginDate = this.beginDate
+      this.featureStore.endDate = this.endDate
+      this.featureStore.thresholdValue = this.thresholdValue
+
+      this.$emit('sendVectorFilter')
     },
     showLayerInfo() {
       this.$emit("sendLayerInfo", this.$props.layer)
@@ -208,36 +204,11 @@ export default defineComponent({
         this.$emit("sendIsVisible")
     }
   },
-  watch: {
-    obsProperty(val) {
-      if (val !== this.selectedProperty) {
-        this.selectedProperty = val
-      }
-    },
-    isCleared(val: boolean) {
-      if (val) {
-        this.clear()
-      }
-    }
-  },
   emits: {
-    sendObsProperty(property: ObservedProperty|undefined){
-      if (property || property === undefined) {
-        return true;
-      } else {
-        console.warn("Invalid StaFilterParams!");
-        return false;
-      }
-    },
-    sendStaFilterParams(params: STAFilterParams){
-      if (params) {
-        return true;
-      } else {
-        console.warn("Invalid StaFilterParams!");
-        return false;
-      }
+    sendVectorFilter(){
+      return true
     },
-    sendLayerInfo(layer: STALayer) {
+    sendLayerInfo(layer: Layer) {
       if (layer) {
         return true
       } else {
@@ -249,8 +220,16 @@ export default defineComponent({
       return true
     }
   },
-  mounted() {
-    this.getSelectableProperties()
+  created() {
+    this.featureStore = useFeatureStore()
+    this.timeseriesStore = useTimeseriesStore()
+
+    if (this.$props.layer.type === 'sta') {
+      this.timeseriesStore.requestStaProperties(this.$i18n.locale)
+    }
+    if (this.$props.layer.type === 'wfs') {
+      this.timeseriesStore.requestWfsProperties()
+    }
   }
 })
 
@@ -258,7 +237,7 @@ export default defineComponent({
 
 <style scoped>
 
-#staControlBar {
+#vectorControlBar {
   position: absolute;
   z-index: 10;
   background: white;
@@ -273,6 +252,10 @@ export default defineComponent({
   display: block !important;
 }
 
+.dropdown-item {
+  max-width: 65vw;
+}
+
 .dropdown-content {
   max-height: 500px;
   overflow-y: scroll;
diff --git a/frontend/src/components/map/vector/VectorLayer.vue b/frontend/src/components/map/vector/VectorLayer.vue
new file mode 100644
index 0000000000000000000000000000000000000000..94bc2ad6757206b085d5caf7e46b6ea4228d60bc
--- /dev/null
+++ b/frontend/src/components/map/vector/VectorLayer.vue
@@ -0,0 +1,225 @@
+<template>
+
+	<ol-interaction-select
+    @select="featureSelected"
+		:condition="selectConditionClick"
+		:filter="selectInteractionFilterClick"
+	>
+    <ol-style :overrideStyleFunction="getSelectStyle">
+    </ol-style>
+	</ol-interaction-select>
+
+	<ol-interaction-select
+    @select="featureSelected"
+		:condition="selectConditionHover"
+		:filter="selectInteractionFilterHover"
+    v-if="!showCluster"
+	>
+
+		<ol-style :overrideStyleFunction="getHoverStyle">
+		</ol-style>
+	</ol-interaction-select>
+
+	<ol-vector-layer
+      :updateWhileAnimating="true"
+      :updateWhileInteracting="true"
+      :z-index="30"
+  >
+		<ol-source-vector>
+
+      <ol-animation-fade :duration="1000">
+
+        <ol-feature
+            v-for="(point, i) in featureStore.features"
+            :key="'point' + i"
+            :properties="point.properties"
+            v-if="!showCluster"
+        >
+          <ol-geom-point :coordinates="point.coordinates"></ol-geom-point>
+
+          <ol-style>
+            <ol-style-circle :radius="10">
+              <ol-style-stroke color="#00589C" :width="4"></ol-style-stroke>
+              <ol-style-fill color="#FFFFFF"></ol-style-fill>
+            </ol-style-circle>
+          </ol-style>
+        </ol-feature>
+
+        <ol-feature
+            v-for="(cluster, i) in featureStore.clusters"
+            :key="'cluster' + i"
+            v-if="showCluster"
+        >
+          <ol-geom-point :coordinates="cluster.coords" v-if="cluster.requestId === featureStore.currentRequestId"></ol-geom-point>
+
+          <ol-style>
+            <ol-style-circle :radius="cluster.count / 50 + 30">
+              <ol-style-stroke color="#00589C" :width="5"></ol-style-stroke>
+              <ol-style-fill color="#00589C1A"></ol-style-fill>
+
+            </ol-style-circle>
+            <ol-style-text :text="'' + cluster.count" scale="2">
+              <ol-style-stroke color="white" :width="6"></ol-style-stroke>
+              <ol-style-fill color="#333"></ol-style-fill>
+            </ol-style-text>
+          </ol-style>
+
+        </ol-feature>
+
+      </ol-animation-fade>
+		</ol-source-vector>
+  </ol-vector-layer>
+
+</template>
+
+<script lang="ts">
+import {defineComponent, inject, PropType} from "vue";
+import Circle from "ol/style/Circle";
+import Fill from "ol/style/Fill";
+import Stroke from "ol/style/Stroke";
+import Style from "ol/style/Style";
+import {TimeseriesStore, useTimeseriesStore} from "@/stores/timeseries.ts";
+import {FeatureStore, useFeatureStore} from "@/stores/feature.ts";
+import {Coordinate, Layer} from "@/types";
+
+
+export default defineComponent({
+	props: {
+    layer: Object as PropType<Layer>
+	},
+  data() {
+    return {
+      featureIdField: '',
+      hoveredThingId: 0,
+      featureStore: {} as FeatureStore,
+      timeseriesStore: {} as TimeseriesStore
+    }
+  },
+  computed: {
+    showCluster() {
+      return this.featureStore.clusters.length > 0
+    },
+  },
+  methods: {
+    selectInteractionFilterHover(feature: any) {
+      if (feature.values_[this.featureIdField]) {
+        this.hoveredThingId = feature.values_[this.featureIdField]
+      }
+      return true
+    },
+    selectInteractionFilterClick(feature: any) {
+
+      if (this.showCluster) {
+
+        this.$emit('zoomCluster', [
+            feature.values_.geometry.flatCoordinates[0],
+            feature.values_.geometry.flatCoordinates[1]
+        ])
+
+      } else {
+
+        if (feature.values_[this.featureIdField]) {
+          if (feature.values_[this.featureIdField] === this.hoveredThingId) {
+
+            if (this.$props.layer?.type === 'sta') {
+              this.timeseriesStore.setStaFeature(feature)
+            }
+            if (this.$props.layer?.type === 'wfs') {
+              this.timeseriesStore.setFeature(feature)
+            }
+
+            this.$emit('sendFeatureInformation')
+          }
+        }
+      }
+    },
+    getStyleCircle(feature: any, borderWidth: number, borderColor: string) {
+
+      let radius = 10
+
+      if (this.$props.layer.type === 'sta') {
+
+        const lastObservationResult = feature.values_['Datastreams/0/Observations/0/result']
+
+        for (const staColor of this.$props.layer.legend) {
+            if (lastObservationResult < staColor.upper_limit) {
+              radius = staColor.radius
+
+              if (staColor.color != '#FFFFFF') {
+                borderColor = staColor.color
+              }
+            }
+        }
+      }
+
+      feature.setStyle(new Style({
+          image: new Circle({
+            radius: radius,
+            fill: new Fill({
+              color: (borderColor == 'red') ? 'white' : '#ffffff6e'
+            }),
+            stroke: new Stroke({
+              color: borderColor,
+              width: borderWidth
+            })
+          })
+      }))
+    },
+    getHoverStyle(feature: any) {
+      this.getStyleCircle(feature, 8, 'red')
+    },
+    getSelectStyle(feature: any) {
+      this.getStyleCircle(feature, 8, 'red')
+    }
+  },
+  emits: {
+    sendFeatureInformation() {
+      return true
+    },
+    zoomCluster(coord: Coordinate) {
+      if (coord) {
+        return true
+      } else {
+        console.warn('Invalid submit event payload!')
+        return false
+      }
+    }
+  },
+  created() {
+    this.featureStore = useFeatureStore()
+    this.timeseriesStore = useTimeseriesStore()
+
+    if (this.$props.layer.type === 'sta') {
+      this.featureIdField = 'id'
+      this.featureStore.requestStaFeatures()
+    }
+    if (this.$props.layer.type === 'wfs') {
+      this.featureIdField = 'fid'
+      this.featureStore.requestWfsFeatures()
+    }
+  },
+  setup() {
+		const strategy: any = inject("ol-loadingstrategy");
+		const bbox = strategy.bbox;
+
+		const format: any = inject("ol-format");
+		const GeoJSON = new format.GeoJSON();
+
+		const selectConditions: any = inject("ol-selectconditions")
+		const selectConditionClick = selectConditions.click
+
+		const selectConditionHover = selectConditions.pointerMove
+
+    const featureSelected = () => {}
+
+		return {
+      bbox,
+			GeoJSON,
+			selectConditionClick,
+			selectConditionHover,
+			featureSelected
+		};
+	},
+});
+
+</script>
diff --git a/frontend/src/components/map/wms/AggregationWidget.vue b/frontend/src/components/map/wms/AggregationWidget.vue
index dd9ae7de6193c4a9b456207304691ad7473ab130..4cb73a837d22803131589ecb6a18e4f1570674df 100644
--- a/frontend/src/components/map/wms/AggregationWidget.vue
+++ b/frontend/src/components/map/wms/AggregationWidget.vue
@@ -15,7 +15,7 @@
         <div>
           <p class="has-text-weight-bold">{{ title }}</p>
           <div v-if="areCountriesVisible">
-            <button class="button is-small mt-2" @click="showDistricts"><b>{{ $t("message.activateDistricts") }}</b></button>
+            <button class="button is-small mt-2 standard-button" @click="showDistricts"><b>{{ $t("message.activateDistricts") }}</b></button>
           </div>
         </div>
 
@@ -195,3 +195,12 @@ export default defineComponent({
 });
 
 </script>
+
+<style scoped>
+
+#aggregationWidgetContent {
+  background: var(--second-background-color);
+  color: var(--main-text-color);
+}
+
+</style>
diff --git a/frontend/src/components/map/wms/OpacityControl.vue b/frontend/src/components/map/wms/OpacityControl.vue
index 49e5c232f846db241838d1b61eb8cf35525885f3..19c938ee346ef52792f866b970c2cbcd4c8b81cf 100644
--- a/frontend/src/components/map/wms/OpacityControl.vue
+++ b/frontend/src/components/map/wms/OpacityControl.vue
@@ -37,7 +37,7 @@ export default defineComponent({
 
 #opacityControl {
   right: .5em;
-  top: .5em;
+  top: calc(.5em + var(--controls-offset));
   pointer-events: auto;
 }
 
diff --git a/frontend/src/components/map/wms/StickyPopupControl.vue b/frontend/src/components/map/wms/StickyPopupControl.vue
index 8351fea74a9361edda82848f821150aca1f8d4c8..6cdda1158dd5d268ff9f3de94f168938d1039489 100644
--- a/frontend/src/components/map/wms/StickyPopupControl.vue
+++ b/frontend/src/components/map/wms/StickyPopupControl.vue
@@ -1,8 +1,8 @@
 <template>
   <div id="stickyPopupControl" class="ol-unselectable ol-control">
     <button class="ol-info" type="button" :title="$t('message.activateStickyPopup')" @click="changeStickyPopup">
-        <font-awesome-icon v-show="isActive" :icon="['fas', 'thumbtack']" size="xs" />
-        <svg v-show="!isActive"  xmlns="http://www.w3.org/2000/svg" height="12" width="15" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="#ffffff" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L481.4 352c9.8-.4 18.9-5.3 24.6-13.3c6-8.3 7.7-19.1 4.4-28.8l-1-3c-13.8-41.5-42.8-74.8-79.5-94.7L418.5 64 448 64c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l29.5 0-6.1 79.5L38.8 5.1zM324.9 352L177.1 235.6c-20.9 18.9-37.2 43.3-46.5 71.3l-1 3c-3.3 9.8-1.6 20.5 4.4 28.8s15.7 13.3 26 13.3l164.9 0zM288 384l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96-64 0z"/></svg>
+        <font-awesome-icon v-show="isActive" :icon="['fas', 'thumbtack']" size="xs" style="color: var(--ol-subtle-foreground-color) !important;" />
+        <svg v-show="!isActive" xmlns="http://www.w3.org/2000/svg" height="12" width="15" viewBox="0 0 640 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path fill="var(--ol-subtle-foreground-color)" d="M38.8 5.1C28.4-3.1 13.3-1.2 5.1 9.2S-1.2 34.7 9.2 42.9l592 464c10.4 8.2 25.5 6.3 33.7-4.1s6.3-25.5-4.1-33.7L481.4 352c9.8-.4 18.9-5.3 24.6-13.3c6-8.3 7.7-19.1 4.4-28.8l-1-3c-13.8-41.5-42.8-74.8-79.5-94.7L418.5 64 448 64c17.7 0 32-14.3 32-32s-14.3-32-32-32L192 0c-17.7 0-32 14.3-32 32s14.3 32 32 32l29.5 0-6.1 79.5L38.8 5.1zM324.9 352L177.1 235.6c-20.9 18.9-37.2 43.3-46.5 71.3l-1 3c-3.3 9.8-1.6 20.5 4.4 28.8s15.7 13.3 26 13.3l164.9 0zM288 384l0 96c0 17.7 14.3 32 32 32s32-14.3 32-32l0-96-64 0z"/></svg>
     </button>
   </div>
 </template>
@@ -37,7 +37,7 @@ export default defineComponent({
 
 #stickyPopupControl {
   right: .5em;
-  top: 2.5em;
+  top: calc(2.5em + var(--controls-offset));
   pointer-events: auto;
 }
 
diff --git a/frontend/src/components/map/wms/WmsTimeControl.vue b/frontend/src/components/map/wms/WmsTimeControl.vue
index d6a87b0c51282928fe69d9697e240027eaeb5c36..7b835d6255c7cb42b08e77c8c19fa7f4598b7d0a 100644
--- a/frontend/src/components/map/wms/WmsTimeControl.vue
+++ b/frontend/src/components/map/wms/WmsTimeControl.vue
@@ -11,7 +11,7 @@
       <span class="tag switchLabel" v-show="isStaControlActive">{{ $t("message.rasterData") }}</span>
 
       <div v-if="timeSteps.length == 1" class="pb-2" :class="compactControlBarStyle">
-            <span class="tag is-main-color sliderLabel">
+            <span class="tag sliderLabel">
               {{ convertTime(timeSteps[0]) }}
             </span>
       </div>
@@ -30,7 +30,7 @@
             </div>
           </div>
 
-          <span class="tag sliderLabel is-main-color">{{ convertTime(selectedTime) }}</span>
+          <span class="tag sliderLabel">{{ convertTime(selectedTime) }}</span>
         </div>
 
         <div v-if="timeSteps.length >= sliderLimit || !isTabletSize" style="display: block" class="select is-small pl-1 pr-1 mt-1" :class="compactControlBarStyle">
@@ -182,7 +182,7 @@ export default defineComponent({
 
 .timeControl {
   overflow: hidden;
-  background: white;
+  background: var(--second-background-color);
   position: absolute;
   transform: translate(calc(50vw - 50%));
   border-radius: 4px;
@@ -236,6 +236,9 @@ export default defineComponent({
   margin-left: 6px;
   margin-right: 7px;
   width: 100px;
+
+  background: var(--active-background-color);
+  color: var(--header-text-highlight-color);
 }
 
 .controlBarMarginLeft {
@@ -252,12 +255,12 @@ export default defineComponent({
 
 .sliderScaleLine > div {
   width: inherit;
-  border-left: 1px solid lightgray;
+  border-left: 1px solid var(--outline-color);
   height: 6px;
 }
 
 .sliderScaleLine > div:last-child {
-  border-right: 1px solid lightgray;
+  border-right: 1px solid var(--outline-color);
 }
 
 .switch-button {
@@ -272,9 +275,14 @@ export default defineComponent({
   float: left;
 }
 
-.is-main-color {
-  color: white;
-  background: var(--main-color);
+input[type="range"].slider::-moz-range-thumb {
+  background: var(--active-background-color);
+  border-radius: 12px;
+  border: none;
+}
+
+input[type="range"].slider::-moz-range-track {
+  background: var(--outline-color);
 }
 
 </style>
diff --git a/frontend/src/components/map/wms/processes/PreAnomaly.vue b/frontend/src/components/map/wms/processes/PreAnomaly.vue
index 5a5c027e66025887c3f5cf3744533f0723e4e33e..4fc109b33b7303008248382593bb95ae623c205b 100644
--- a/frontend/src/components/map/wms/processes/PreAnomaly.vue
+++ b/frontend/src/components/map/wms/processes/PreAnomaly.vue
@@ -22,7 +22,7 @@
     </div>
 
     <button
-      class="button is-small mt-2 mb-2"
+      class="button is-small mt-2 mb-2 standard-button"
       @click.prevent="requestAnomaly"
       v-show="!isLoading"
     >
diff --git a/frontend/src/components/map/wms/processes/ResultMessage.vue b/frontend/src/components/map/wms/processes/ResultMessage.vue
index 1af694dd9b5493d484b78faf0d2915cc7bb49677..1efa7ad123762baaa29904d5ff0ab8e483b5dd3e 100644
--- a/frontend/src/components/map/wms/processes/ResultMessage.vue
+++ b/frontend/src/components/map/wms/processes/ResultMessage.vue
@@ -33,8 +33,8 @@ export default defineComponent({
 
   border: 4px solid rgba(10, 10, 10, 0.1);
   border-radius: 50%;
-  border-top: 4px solid var(--main-color);
-  border-bottom: 4px solid var(--main-color);
+  border-top: 4px solid var(--active-background-color);
+  border-bottom: 4px solid var(--active-background-color);
 
   animation: spin 2s linear infinite;
 }
diff --git a/frontend/src/components/map/wms/processes/StandardAggregation.vue b/frontend/src/components/map/wms/processes/StandardAggregation.vue
index ed47259bbd71daf698a1e78610b76b297806f6f1..0e3e4565cb43a91c6405cbe13beaee1a4b13d245 100644
--- a/frontend/src/components/map/wms/processes/StandardAggregation.vue
+++ b/frontend/src/components/map/wms/processes/StandardAggregation.vue
@@ -22,7 +22,7 @@
     <div>
       <button
         :disabled="checkedAggregationTypes.length === 0"
-        class="button is-small mt-2 mb-2"
+        class="button is-small mt-2 mb-2 standard-button"
         @click.prevent="requestAggregationValues"
         v-show="!isLoading"
       >
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
index 8460affbe5a38529d8ef6e45369c5449c1756bd8..9bd0cd1368e7caf057cc2e70265dfc1e4d63563b 100644
--- a/frontend/src/main.ts
+++ b/frontend/src/main.ts
@@ -4,7 +4,7 @@ import { createApp } from "vue";
 import App from "./App.vue";
 import router from "./router";
 import { createI18n } from 'vue-i18n'
-
+import { createPinia } from 'pinia'
 import 'bulma/css/bulma.css'
 
 /* import the fontawesome core */
@@ -63,10 +63,13 @@ const i18n = createI18n({
 })
 
 const app = createApp(App);
+const pinia = createPinia()
+
 app.use(router);
 app.use(OpenLayersMap);
 app.use(drag);
 app.use(i18n);
+app.use(pinia)
 
 app.component('font-awesome-icon', FontAwesomeIcon)
 
diff --git a/frontend/src/stores/feature.ts b/frontend/src/stores/feature.ts
new file mode 100644
index 0000000000000000000000000000000000000000..badada669cdc60a84fc700b9a2062d58e3061b88
--- /dev/null
+++ b/frontend/src/stores/feature.ts
@@ -0,0 +1,251 @@
+import {defineStore} from 'pinia'
+import axios from "axios";
+import proj4 from "proj4";
+import {Cluster, MatrixCell, STALayer, VectorProperty, WFSLayer} from "@/types/vector";
+import {BoundingBox} from "@/types";
+
+
+const MAX_LOCATIONS_ON_DESKTOP = 300
+const MAX_LOCATIONS_ON_MOBILE = 200
+
+const MATRIX_WIDTH = 5
+const MATRIX_HEIGHT = 3
+const MATRIX: MatrixCell[] = [
+    [[0, 2], [1, 3], 1],
+    [[0, 1], [1, 2], 2],
+    [[0, 0], [1, 1], 3],
+
+    [[1, 2], [2, 3], 4],
+    [[1, 1], [2, 2], 5],
+    [[1, 0], [2, 1], 6],
+
+    [[2, 2], [3, 3], 7],
+    [[2, 1], [3, 2], 8],
+    [[2, 0], [3, 1], 9],
+
+    [[3, 2], [4, 3], 10],
+    [[3, 1], [4, 2], 11],
+    [[3, 0], [4, 1], 12],
+
+    [[4, 2], [5, 3], 13],
+    [[4, 1], [5, 2], 14],
+    [[4, 0], [5, 1], 15],
+]
+
+export interface FeatureStore {
+    wfsLayer: WFSLayer|null,
+    staLayer: STALayer|null,
+    clusters: Cluster[],
+    features: any[],
+    featuresCount: number,
+    selectedPropertyId: number|undefined,
+    beginDate: string,
+    endDate: string,
+    thresholdValue: string,
+    currentBoundingBox: BoundingBox,
+    currentRequestId: number,
+    isLoading: boolean
+}
+
+export const useFeatureStore = defineStore('feature', {
+
+    state: (): FeatureStore => ({
+        wfsLayer: null,
+        staLayer: null,
+        clusters: [],
+        features: [],
+        featuresCount: 0,
+        selectedPropertyId: undefined,
+        beginDate: '',
+        endDate: '',
+        thresholdValue: '',
+        currentBoundingBox: [[0, 0], [90, 90]] as BoundingBox,
+        currentRequestId: 0 as number,
+        isLoading: false
+    }),
+    getters: {
+        baseUrl() {
+            return import.meta.env.VITE_APP_BACKEND_URL + 'wfs-features/'+ this.wfsLayer.workspace + '/'+ this.wfsLayer.prefix + '?'
+        },
+        bboxBottomLeftLon() {
+            return this.currentBoundingBox[0][0]
+        },
+        bboxBottomLeftLat() {
+            return this.currentBoundingBox[0][1]
+        },
+        bboxTopRightLon() {
+            return this.currentBoundingBox[1][0]
+        },
+        bboxTopRightLat() {
+            return this.currentBoundingBox[1][1]
+        },
+        cellWidth() {
+            return Math.abs((this.bboxBottomLeftLon - this.bboxTopRightLon) / MATRIX_WIDTH)
+        },
+        cellHeight() {
+            return Math.abs((this.bboxBottomLeftLat - this.bboxTopRightLat) / MATRIX_HEIGHT)
+        },
+        maxLocations() {
+            return (window.innerWidth > 1408) ? MAX_LOCATIONS_ON_DESKTOP : MAX_LOCATIONS_ON_MOBILE
+        }
+    },
+    actions: {
+        setSelectedProperty(property: VectorProperty|undefined) {
+            this.featuresCount = 0
+
+            if (property) {
+                this.selectedPropertyId = property.id
+            } else {
+                this.selectedPropertyId = undefined
+            }
+
+            if (this.wfsLayer) {
+                this.requestWfsFeatures()
+            }
+            if (this.staLayer) {
+                this.requestStaFeatures()
+            }
+        },
+        requestWfsFeatures() {
+            const wfsParams = [
+                'service=WFS',
+                'outputFormat=application%2Fjson',
+                'srs=EPSG:4326'
+            ]
+            const url = this.baseUrl + wfsParams.join('&') + '&'
+
+            this.requestFeatures(url, 'totalFeatures')
+        },
+        requestStaFeatures() {
+            const url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-locations/' + this.staLayer.own_id + '?'
+
+            this.requestFeatures(url, '@iot.count')
+        },
+        requestFeatures(url: string, countField: string) {
+
+            const countUrl = url + this.getFilterParams() + '&count=1'
+
+            axios.get(countUrl).then(response => {
+
+                this.featuresCount = response.data[countField]
+
+                this.isLoading = true
+
+                this.clusters = []
+                this.features = []
+
+                if (this.featuresCount > this.maxLocations || this.featuresCount === 0) {
+                    this.requestCluster(url, countField)
+                } else {
+                    this.requestFullLocations(url)
+                }
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                })
+        },
+        getFilterParams(cell: MatrixCell | undefined = undefined) {
+
+            const params = []
+
+            if (this.thresholdValue !== '') {
+            params.push('threshold=' + this.thresholdValue)
+            }
+            if (this.beginDate !== '') {
+            params.push('beginDate=' + this.beginDate)
+            }
+            if (this.endDate !== '') {
+            params.push('endDate=' + this.endDate)
+            }
+
+            if (cell) {
+                const cellBottomLon = this.bboxBottomLeftLon + (this.cellWidth * cell[0][0])
+                const cellBottomLat = this.bboxBottomLeftLat + (this.cellHeight * cell[0][1])
+
+                const cellTopLon = this.bboxBottomLeftLon + (this.cellWidth * cell[1][0])
+                const cellTopLat = this.bboxBottomLeftLat + (this.cellHeight * cell[1][1])
+
+                params.push('bbox=' + cellBottomLon + ',' + cellBottomLat + ',' + cellTopLon + ',' + cellTopLat + ',EPSG:4326')
+            } else {
+                params.push('bbox=' + this.bboxBottomLeftLon + ',' + this.bboxBottomLeftLat + ',' + this.bboxTopRightLon + ',' + this.bboxTopRightLat + ',EPSG:4326')
+            }
+
+            if (this.selectedPropertyId) {
+                params.push('property=' + this.selectedPropertyId)
+            }
+
+            return params.join('&')
+        },
+        requestCluster(url: string, countField: string) {
+
+            const requestId = Math.floor(Math.random() * 1000000)
+            this.currentRequestId = requestId
+
+            let countFinishedRequests = 0
+
+            for (const cell of MATRIX) {
+
+                let clusterUrl = url + this.getFilterParams(cell) + '&count=1'
+
+                axios.get(clusterUrl).then(response => {
+
+                        const countFeatures = response.data[countField]
+
+                        if (countFeatures > 0 && requestId === this.currentRequestId) {
+
+                            if (response.data.features[0]) {
+
+                                const newCluster = {
+                                    coords: proj4('WGS84', 'EPSG:3857', response.data.features[0].geometry.coordinates),
+                                    coordsWgs84: response.data.features[0].geometry.coordinates,
+                                    count: countFeatures,
+                                    requestId: requestId,
+                                    cellNumber: cell[2]
+                                }
+
+                                if (requestId === this.currentRequestId) {
+                                    this.clusters.push(newCluster)
+                                }
+                            }
+                        }
+                    })
+                    .catch((error: any) => {
+                        console.log(error)
+                        this.isLoading = false
+                    })
+                    .finally(() => {
+                        countFinishedRequests++
+                        if (countFinishedRequests === MATRIX.length) {
+                            this.isLoading = false
+                        }
+                    })
+            }
+        },
+        requestFullLocations(url: string) {
+
+            const locationUrl = url + this.getFilterParams()
+
+            axios.get(locationUrl)
+                .then(response => {
+                    if (response.data['features']) {
+                        for (const point of response.data['features']) {
+                            if (this.wfsLayer) {
+                                point.properties['fid'] = point['id'].replace(this.wfsLayer.prefix + '_feature.', '')
+                            }
+                            this.features.push({
+                                coordinates: proj4('WGS84', 'EPSG:3857', point.geometry.coordinates),
+                                properties: point.properties
+                            })
+                        }
+                    }
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                    this.isLoading = false
+                })
+                .finally(() => {
+                    this.isLoading = false
+                })
+        }
+    }
+})
diff --git a/frontend/src/stores/timeseries.ts b/frontend/src/stores/timeseries.ts
new file mode 100644
index 0000000000000000000000000000000000000000..eadf3770c2893a87b3860dedf1aa40eba4123b90
--- /dev/null
+++ b/frontend/src/stores/timeseries.ts
@@ -0,0 +1,344 @@
+import {defineStore} from 'pinia'
+import axios from "axios";
+import {Coordinate, CovJsonSingleResult, DataCollection, WMSLayer} from "@/types";
+import {convertTime} from "@/composables/time_utils.ts";
+import {STALayer, StaPopupInfo, StaThing, VectorProperty, WFSLayer, WfsTimeseriesFeature} from "@/types/vector";
+
+export interface TimeseriesStore {
+    wfsLayer: WFSLayer|null,
+    staLayer: STALayer|null,
+    feature: {},
+    featureId: number|undefined,
+    selectedTimeseriesId: number|undefined,
+    locationProperties: VectorProperty[],
+    layerProperties: VectorProperty[],
+    selectedProperty: VectorProperty|undefined,
+    dataCollection: DataCollection,
+    inputPlaceholder: string,
+    isChartLoading: boolean,
+    clickedLayer: WMSLayer|undefined,
+    clickedLayerTime: string|undefined,
+    clickedCoord: Coordinate|undefined,
+    staThing: StaThing|undefined,
+    isClickOnFeature: boolean,
+    popupInfos: StaPopupInfo[]
+}
+
+export const useTimeseriesStore = defineStore('timeseries', {
+
+    state: (): TimeseriesStore => ({
+        wfsLayer: null,
+        staLayer: null,
+        feature: {},
+        featureId: undefined,
+        selectedTimeseriesId: undefined,
+        locationProperties: [] as VectorProperty[],
+        layerProperties: [] as VectorProperty[],
+        selectedProperty: {} as VectorProperty,
+        dataCollection: {} as DataCollection,
+        inputPlaceholder: '-',
+        isChartLoading: false,
+
+        clickedLayer: undefined,
+        clickedLayerTime: undefined,
+        clickedCoord: undefined,
+
+        staThing: undefined,
+        isClickOnFeature: false,
+        popupInfos: [] as StaPopupInfo[]
+    }),
+    getters: {
+        baseUrl() {
+            return import.meta.env.VITE_APP_BACKEND_URL + 'proxy/geoserver/' + this.wfsLayer.workspace + '/ows?'
+        },
+        typeName() {
+            return this.wfsLayer.workspace + '%3A' + this.wfsLayer.prefix
+        },
+    },
+    actions: {
+        setFeature(feature: any) {
+            this.featureId = feature.values_.fid
+
+            if (this.featureId !== undefined) {
+
+                this.isClickOnFeature = true
+
+                const params = [
+                    'service=WFS',
+                    'version=2.0.0',
+                    'request=GetFeature',
+                    'typeName=' + this.typeName + '_feature',
+                    'outputFormat=application%2Fjson',
+                    'featureID=' + this.featureId
+                ]
+                const url = this.baseUrl + params.join('&')
+
+                axios.get(url)
+                    .then(response => {
+                        this.feature = response.data.features[0]
+
+                        this.locationProperties = []
+                    })
+                    .catch((error: any) => {
+                        console.log(error)
+                    })
+                    .finally(() => {
+                        this.requestTimeSeriesFeatures()
+                    })
+            }
+        },
+        setStaFeature(feature: any) {
+            let thing = feature.values_
+
+            if (thing.id !== undefined) {
+
+                const url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-thing/' + this.staLayer.endpoint + '/' + thing.id
+
+                axios.get(url)
+                    .then(response => {
+                        thing = response.data.value[0]
+                    })
+                    .catch((error: any) => {
+                        console.log(error)
+                    })
+                    .finally(() => {
+                        this.setStaThing(thing)
+                    })
+            }
+        },
+        requestTimeSeriesFeatures() {
+
+            const params = [
+                'service=WFS',
+                'version=2.0.0',
+                'request=GetFeature',
+                'typeName=' + this.typeName + '_timeseries',
+                'outputFormat=application%2Fjson',
+                'CQL_FILTER=feature_id = ' + this.featureId
+            ]
+            const url = this.baseUrl + params.join('&')
+
+            axios.get(url)
+                .then(response => {
+
+                    if (this.locationProperties.length === 0) {  // following loop gets somehow executed twice
+                        for (const timeseriesFeature of response.data['features']) {
+                            this.locationProperties.push({
+                                id: timeseriesFeature.properties.property_id,
+                                wfsTimeseriesId: timeseriesFeature.id.replace(this.wfsLayer.prefix + '_timeseries.', ''),
+                                fullName: this.getNameOfProperty(timeseriesFeature) + ' (' + timeseriesFeature.properties.unit + ')'
+                            })
+                        }
+                    }
+                    this.locationProperties.sort((a: VectorProperty, b: VectorProperty) => a.fullName.localeCompare(b.fullName))
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                })
+                .finally(() => {
+                    if (this.selectedProperty) {
+                        for (const property of this.locationProperties) {
+                            if (this.selectedProperty.id == property.id) {
+                                this.selectedTimeseriesId = property.wfsTimeseriesId
+                            }
+                        }
+                    }
+                })
+        },
+        getNameOfProperty(timeseriesFeature: WfsTimeseriesFeature) {
+            for (const prop of this.layerProperties) {
+                if (prop.id == timeseriesFeature.properties.property_id) {
+                    return prop.fullName
+                }
+            }
+            return timeseriesFeature.id
+        },
+        setSelectedProperty(property: VectorProperty|undefined) {
+            this.selectedProperty = property
+
+            if (property) {
+                this.inputPlaceholder = property.fullName
+                this.selectedTimeseriesId = property.wfsTimeseriesId
+
+            } else {
+                this.inputPlaceholder = '-'
+                this.selectedTimeseriesId = undefined
+            }
+
+            if (this.staThing) {
+                this.isClickOnFeature = true
+            }
+        },
+        requestWfsProperties() {
+
+            this.layerProperties = []
+
+            const params = [
+                'service=WFS',
+                'version=2.0.0',
+                'request=GetFeature',
+                'typeName=' + this.typeName + '_property',
+                'outputFormat=application%2Fjson'
+            ]
+            const url = this.baseUrl + params.join('&')
+
+            axios.get(url)
+                .then(response => {
+                    for (const property of response.data['features']) {
+                        this.layerProperties.push({
+                            id: property['id'].replace(this.wfsLayer.prefix + '_property.', ''),
+                            fullName: property['properties']['name']
+                        })
+                    }
+                    this.layerProperties.sort((a: VectorProperty, b: VectorProperty) => a.fullName.localeCompare(b.fullName))
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                })
+        },
+        requestStaProperties(locale: string) {
+            this.layerProperties = []
+
+            let url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-obs-properties/' + this.staLayer.own_id
+
+            axios.get(url)
+                .then(response => {
+
+                    const name_prop = (locale == 'de') ? 'name_de' : 'name_en'
+                    for (const property of response.data.value) {
+
+                        let fullName = property.name
+                        if (property.properties) {
+                            if (property.properties[name_prop]) {
+                                fullName = property.properties[name_prop]
+                            }
+                        }
+                        this.layerProperties.push({
+                            id: property['@iot.id'],
+                            fullName: fullName
+                        })
+                    }
+                    this.layerProperties.sort((a: VectorProperty, b: VectorProperty) => a.fullName.localeCompare(b.fullName))
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                })
+        },
+        setStaThing(thing: StaThing) {
+            this.staThing = thing
+            this.isClickOnFeature = true
+            this.requestThingProperties(thing)
+            this.requestPointData()
+        },
+        requestThingProperties(thing: StaThing) {
+
+            let url = import.meta.env.VITE_APP_BACKEND_URL + 'sta-obs-properties-by-thing/' + this.staLayer.own_id + '/' + thing['@iot.id']
+
+            this.locationProperties = []
+
+            axios.get(url)
+                .then(response => {
+                    this.setPopupInfos(thing)
+
+                    for (const obsProperty of response.data.value) {
+                        this.locationProperties.push({
+                            id: obsProperty['@iot.id'],
+                            fullName: obsProperty['name']
+                        })
+                    }
+                    this.locationProperties.sort((a: VectorProperty, b: VectorProperty) => a.fullName.localeCompare(b.fullName))
+                })
+                .catch((error: any) => {
+                    console.log(error)
+                })
+                .finally(() => {
+                    if (this.locationProperties.length == 1) {
+                        this.isClickOnFeature = true
+                        this.selectedProperty = this.locationProperties[0]
+                        this.requestPointData()
+                    }
+                })
+        },
+        setPopupInfos(thing: StaThing) {
+            this.popupInfos = []
+            for (const key of Object.keys(thing.properties)) {
+
+                let infoText = thing.properties[key]
+                if (infoText instanceof Array) {
+                    infoText = infoText.join(', ')
+                }
+                this.popupInfos.push({
+                    label: key,
+                    text: infoText
+                })
+            }
+        },
+        requestPointData() {
+
+            const params: string[] = []
+
+            if (this.clickedLayer && this.clickedLayerTime && this.clickedCoord) {
+                params.push('wmsLayer=' + this.clickedLayer.own_id)
+                params.push('time=' + this.clickedLayerTime)
+                params.push('x=' + this.clickedCoord[0])
+                params.push('y=' + this.clickedCoord[1])
+            }
+
+            if (this.isClickOnFeature && this.staLayer && this.staThing && this.selectedProperty) {
+                params.push('staLayer=' + this.staLayer.own_id)
+                params.push('staThing=' + this.staThing['@iot.id'])
+                params.push('staObsProperty=' + this.selectedProperty.id)
+            }
+
+            if (this.isClickOnFeature && this.wfsLayer && this.selectedProperty && this.selectedTimeseriesId) {
+                params.push('workspace=' + this.wfsLayer.workspace)
+                params.push('prefix=' + this.wfsLayer.prefix)
+                params.push('wfsTimeSeries=' + this.selectedTimeseriesId)
+            }
+
+            if (params.length > 0) {
+
+                this.isChartLoading = true
+
+                // const url = import.meta.env.VITE_APP_BACKEND_URL + 'timeseries/' + this.$i18n.locale + '?' + params.join('&')
+                const url = import.meta.env.VITE_APP_BACKEND_URL + 'timeseries/de?' + params.join('&')
+
+                axios.get(url)
+                    .then(response => {
+                        this.dataCollection = response.data
+                        if (this.dataCollection.mapPopupResult) {
+                            this.setTimeFormatInPopup(this.dataCollection.mapPopupResult)
+                        }
+                        if (this.dataCollection.timeSeriesCollection[0]) {
+                            if (this.dataCollection.timeSeriesCollection[0].name === 'wfs') {
+                                this.dataCollection.timeSeriesCollection[0].name = this.selectedProperty.fullName
+                                // this.dataCollection.timeSeriesCollection[0].unit = this.selectedTimeseries.unit
+                            }
+                        }
+                    })
+                    .catch((error) => {
+                        console.log(error)
+                    })
+                    .finally(() => {
+                        this.isChartLoading = false
+                        this.isClickOnFeature = false
+                    })
+            }
+        },
+        setTimeFormatInPopup(result: CovJsonSingleResult) {
+            this.dataCollection.mapPopupResult.time = convertTime(
+                result.time,
+                'de',
+                result.timeFormat
+            )
+        },
+        unsetTimeseriesData() {
+            this.dataCollection = {
+                chartName: '',
+                mapPopupResult: {},
+                timeSeriesCollection: [],
+                singleResultCollection: []
+            }
+        }
+    }
+})
\ No newline at end of file
diff --git a/frontend/src/translation/messages.ts b/frontend/src/translation/messages.ts
index 3754c2edd01c05f7842454351969463290bb441b..b07496a362e9b04cca4b5a0a69ed58e15a8098ac 100644
--- a/frontend/src/translation/messages.ts
+++ b/frontend/src/translation/messages.ts
@@ -31,6 +31,8 @@ export const messages = {
       filterOptions: 'Filteroptionen',
       imprint: 'Impressum',
       invalidDate: 'Ungültiges Datumsformat!',
+      invalidProperty: 'Keine Zeitreihe ausgewählt!',
+      invalidThreshold: 'Keine Zahl eingegeben!',
       legend: 'Legende',
       map: 'Karte',
       measurementsFrom: 'Messwerte ab Datum',
@@ -40,7 +42,6 @@ export const messages = {
       noMethodAvailable: 'Keine Methode verfügbar',
       noLayerSelectable: 'Kein Layer auswählbar',
       noPointRequest: 'Klimasimulationen sind am Punkt nicht aussagekräftig. Bitte nutzen Sie die Möglichkeit der Auswertung für Bundesländer und Landkreise oder laden Sie ein eigenes Shapefile hoch.',
-      observations: 'Observationen',
       pointData: 'Punktdaten',
       precipitationAnomaly: 'Niederschlagsanomalien',
       printChart: 'Diagramm als Bild herunterladen',
@@ -120,6 +121,8 @@ export const messages = {
       filterOptions: 'Filter Options',
       imprint: 'Imprint',
       invalidDate: 'Invalid date!',
+      invalidProperty: 'No Timeseries selected!',
+      invalidThreshold: 'Not a number!',
       legend: 'Legend',
       map: 'Map',
       measurementsFrom: 'Measurements from date',
@@ -129,7 +132,6 @@ export const messages = {
       noMethodAvailable: 'No method available',
       noLayerSelectable: 'No layer selectable',
       noPointRequest: 'Climate simulations cannot be interpreted at the grid cell level. Please use the regional analysis to define your region of interest.',
-      observations: 'Observations',
       pointData: 'Point Data',
       precipitationAnomaly: 'Precipitation Anomaly',
       printChart: 'Download diagram as image',
diff --git a/frontend/src/types/index.d.ts b/frontend/src/types/index.d.ts
index f7fccf372d2cfe5a4587e83fcb47df92ea224d78..ff2a2cda25b0db8bafb53ec55d629e445c416613 100644
--- a/frontend/src/types/index.d.ts
+++ b/frontend/src/types/index.d.ts
@@ -16,8 +16,6 @@ export interface AreaName {
 export interface Area {
     id: number
     name: string
-    activeBaseMap: string
-    baseMaps: []
     layers: Layer[]
     info: string
     is_default_on_start: boolean
@@ -46,7 +44,8 @@ export interface ProjectInfo {
     default_lon: number,
     default_lat: number,
     default_zoom: number,
-    baseMap: string,
+    base_layer: string,
+    is_base_layer_gray: string,
     gallery_image: string
 }
 
@@ -58,7 +57,7 @@ export interface Layer {
     time_format: DateFormat
     y_axis_min: number
     y_axis_max: number
-    type: 'wms' | 'sta' | 'geojson'
+    type: 'wms' | 'sta' | 'wfs' | 'geojson'
     is_default_on_start: boolean
 }
 
@@ -92,7 +91,6 @@ export interface GEOJSONLayer extends Layer {
     url: string
 }
 
-
 export interface MapClickEvent {
     coordinate: Coordinate
     pixel: [number, number]
diff --git a/frontend/src/types/sta.d.ts b/frontend/src/types/vector.d.ts
similarity index 58%
rename from frontend/src/types/sta.d.ts
rename to frontend/src/types/vector.d.ts
index 5f32312f9bbc630e88ed0008be989a8154fcb4a5..79069c527e8636c78627bf4aa0f45729e3a91396 100644
--- a/frontend/src/types/sta.d.ts
+++ b/frontend/src/types/vector.d.ts
@@ -16,6 +16,13 @@ export interface STALayer extends Layer {
 }
 
 
+export interface WFSLayer extends Layer {
+    own_id: number
+    workspace: string
+    prefix: string
+}
+
+
 export interface STAObservedProperty {
     "@iot.id": number,
     "definition": string,
@@ -26,23 +33,47 @@ export interface STAObservedProperty {
     }
 }
 
-export interface ObservedProperty {
-    id: number,
-    fullName: string
+
+export interface WfsTimeseriesFeature {
+    "type": "Feature",
+    "id": string,
+    "geometry": {
+        "type": "Point",
+        "coordinates": {
+            0: number,
+            1: number
+        }
+    },
+    "geometry_name": "geom",
+    "properties": {
+        feature_id: number,
+        property_id: number,
+        unit: string,
+        lat: number,
+        lon: number,
+        gtype: "Point",
+        srid: number,
+        min_value: number,
+        max_value: number,
+        min_date: string,
+        max_date: string
+    }
 }
 
 
-export interface STAFilterParams {
-    threshold: string,
-    beginDate: string
-    endDate: string
+export interface VectorProperty {
+    id: number,
+    fullName: string,
+    wfsTimeseriesId: number,
+    type: 'sta'|'wfs'
 }
 
 
 export interface StaThing {
-    "description": string
+    "@iot.selfLink": string
     "@iot.id": number
-    "name": string
+    name: string
+    description: string
     "properties": {
         [key: string]: string|number|string[]
     }
diff --git a/pygeoapi/pygeoapi_conn/pygeoapi-config-prod.yaml b/pygeoapi/pygeoapi_conn/pygeoapi-config-prod.yaml
index b8ab250c67d265680a437b722be33f2eda14757a..0bafa3edeefc472c47b819334df4829a4db92b1f 100755
--- a/pygeoapi/pygeoapi_conn/pygeoapi-config-prod.yaml
+++ b/pygeoapi/pygeoapi_conn/pygeoapi-config-prod.yaml
@@ -31,7 +31,7 @@ server:
   bind:
     host: 0.0.0.0
     port: 5000
-  url: https://gdi-fs.ufz.de/
+  url: https://web.app.ufz.de/
   mimetype: application/json; charset=UTF-8
   encoding: utf-8
   gzip: false