# Map Layer Configuration Update Plan

## Overview

Replace the hardcoded `layersData` arrays in each page's JavaScript with a database-driven approach matching the pattern already used in `map_v5.js`. Layer configuration is centralised in `public.map_layers`; a new junction table `public.map_layer_page_config` controls which layers appear on each page and what per-page overrides apply (editable, allow create, default visibility, snap).

Pages in scope: `projectedit`, `accountedit`, `stocklistedit`, `wayleaveedit`, `opportunityedit`. `map_v5.js` already uses the API; its `loadLayersFromAPI()` function is migrated to the shared utility as the first step, then `map_v5.js` is updated to call it from there.

---

## Core Concepts

| Term | Definition |
|---|---|
| **Base layer config** | Layer properties that do not change per page: WMS URL, layer name, style, z-index, category, tooltip, zoom range, etc. Stored in `public.map_layers`. |
| **Page config** | Per-page overrides: editable, allow create, default visibility, snap to this, focus on load. Stored in `map_layer_page_config`. |
| **Page context** | A string key identifying the page, e.g. `projectedit`, matching the `?do=` route name. |
| **Presence = inclusion** | A layer only appears on a page if a row exists in `map_layer_page_config` for that `(layer_id, page_context)` pair. No row = not shown. |

---

## Database Schema

### New table: `public.map_layer_page_config`

```sql
CREATE TABLE public.map_layer_page_config (
    layer_id         integer NOT NULL REFERENCES public.map_layers(layer_id) ON DELETE CASCADE,
    page_context     text    NOT NULL,
    layer_editable   boolean NOT NULL DEFAULT false,
    layer_allow_create boolean NOT NULL DEFAULT false,
    visible          boolean NOT NULL DEFAULT true,
    snap_to_this     boolean NOT NULL DEFAULT false,
    focus_on_load    boolean NOT NULL DEFAULT false,
    PRIMARY KEY (layer_id, page_context)
);

CREATE INDEX idx_mlpc_page ON public.map_layer_page_config(page_context);
```

No changes are required to `public.map_layers` itself.

---

## Backend: `map_layer_manager.php`

### Modified `getLayers()` function

Add a `page_context` parameter. When provided, JOIN to `map_layer_page_config` so only layers with a row for that page are returned, with per-page columns overriding the base values.

```sql
SELECT
    l.*,
    p.layer_editable,
    p.layer_allow_create,
    p.visible,
    p.snap_to_this,
    p.focus_on_load
FROM public.map_layers l
JOIN public.map_layer_page_config p
    ON p.layer_id = l.layer_id
    AND p.page_context = :page
WHERE l.layer_active = true
ORDER BY l.z_index DESC
```

The existing `get-layers` mode (used by `map_v5.js` without a page parameter) continues to work unchanged — it falls back to the current `SELECT * FROM map_layers WHERE layer_active = true` query.

### New `mode` value

| Mode | Parameter | Returns |
|---|---|---|
| `get-layers` | none | All active layers (existing — map_v5.js) |
| `get-layers-for-page` | `page=projectedit` | Layers for the given page with per-page overrides |

### New CRUD modes for admin UI

| Mode | Action |
|---|---|
| `get-page-configs` | All `map_layer_page_config` rows for a given `layer_id` |
| `save-page-config` | Upsert a `map_layer_page_config` row |
| `delete-page-config` | Delete a `map_layer_page_config` row (removes layer from that page) |

---

## JavaScript Changes

### `www/js/map_layer_utils.js` — shared utility (new file)

Created first. Consolidates three things that are currently duplicated across all map-bearing page files: the API fetch/mapping, the style managers, and the style lookup functions.

---

#### 1. Layer loading

**`loadLayersFromAPI(pageContext)`**
Migrated directly from `map_v5.js` lines 2269–2319. Fetches from `map_layer_manager.php` and maps the raw API response to the `layersData` shape the page JS expects. When `pageContext` is provided, calls `mode=get-layers-for-page&page=<pageContext>`; when omitted, falls back to `mode=get-layers` (preserving backward compatibility for any caller that doesn't use page filtering).

**`mapApiResponseToLayerData(apiLayer)`**
The inner mapping function extracted from the `.map()` call in `loadLayersFromAPI()`. Kept separate so individual pages can call it if they need to remap a single layer without triggering a full fetch.

---

#### 2. Style managers

Currently each file defines its own local `polyStyleManager`, `lineStyleManager`, and `pointStyleManager` objects. These are not identical across files — coverage varies:

| File | polyStyleManager | lineStyleManager | pointStyleManager |
|---|---|---|---|
| `map_v5.js` | yes | yes | yes |
| `project_edit_v2.js` | yes | yes | yes |
| `opportunity_edit.js` | yes | yes | yes |
| `stocklist_edit.js` | — | yes | yes |
| `account_edit_v2.js` | — | yes | yes |

Before moving them to the utils file, a comparison pass is needed to identify:
- Style categories and types that are identical across files (safe to deduplicate as-is)
- Styles that differ between files for the same category/type key (require a decision — keep separate named variants or unify)
- Style categories only present in one file (move across as-is)

The result is a single canonical set of style managers in `map_layer_utils.js`. Where a page currently uses a category that no other page has, that category is still consolidated into the shared object — it just won't be referenced by other pages. Some reworking of inconsistent definitions is expected and accepted.

The `editstyles` array (vertex/editing highlight styles, defined at the top of `map_v5.js` and `project_edit_v2.js`) is also consolidated here.

---

#### 3. Style lookup functions

The three resolver functions that translate `(layerCategory, type)` → OL style object currently live inline in each page's `createLayersAndControls()`. These move to `map_layer_utils.js` as named exports:

- `getPolyStyle(layerCategory, type, labelText)` — wraps `polyStyleManager` lookup with fallback
- `getLineStyle(layerCategory, type, labelText)` — wraps `lineStyleManager` lookup with fallback
- `getPointStyle(layerCategory, type, labelText)` — wraps `pointStyleManager` lookup with fallback

Each page's `createLayersAndControls()` replaces its local inline style resolution with calls to these three functions.

---

`map_layer_utils.js` is loaded via `global_functions.php` on all routes that use a map:

```
projectedit, accountedit, stocklistedit, wayleaveedit, opportunityedit, map
```

`map_v5.js` is updated to remove its local `loadLayersFromAPI()`, style managers, and `editstyles`, calling the equivalents from `MapLayerUtils` instead. This is a net reduction in `map_v5.js` of several hundred lines.

---

### Per-page changes

Each of the five target page JS files currently starts with a large hardcoded `const layersData = [...]`. The change for each file is:

1. Remove the hardcoded `layersData` array.
2. Declare `let layersData = [];` at the top instead.
3. Call `MapLayerUtils.loadLayersFromAPI(pageContext)` — no local implementation needed.
4. Remove the local `lineStyleManager`, `pointStyleManager`, `polyStyleManager` definitions.
5. Replace inline style resolution in `createLayersAndControls()` with `MapLayerUtils.getLineStyle()` / `getPointStyle()` / `getPolyStyle()`.
6. The existing `createLayersAndControls(map, layersData, ...)` call moves inside the `.then()` callback.

### Per-file page context strings

| File | Page context |
|---|---|
| `project_edit_v2.js` | `projectedit` |
| `account_edit_v2.js` | `accountedit` |
| `stocklist_edit.js` | `stocklistedit` |
| `wayleave_edit_v2.js` | `wayleaveedit` |
| `opportunity_edit.js` | `opportunityedit` |

### Special case: `wayleave_edit_v2.js`

This file is already partially migrated — it calls `map_layer_manager.php?mode=get-layers` via `WL.fetchLayers()` and renders base/reference layers from the API. It does **not** have a hardcoded `layersData` array.

Two changes are needed rather than a full port:

1. **Replace `WL.fetchLayers()` with `MapLayerUtils.loadLayersFromAPI('wayleaveedit')`** — retire the local fetch implementation and use the shared utility, which handles the page filtering automatically.

2. **Add vector layer support** — `WL.createMapLayerControls()` currently filters to `['tile','xyz','wms']` only (line 92), silently dropping any vector layers. Extend it to handle `vectorPoly`, `vectorLine`, `vectorPoint` layer types using the same pattern as `project_edit_v2.js`'s `createLayersAndControls()`, including applying the per-page `layer_editable` and `layer_allow_create` values returned by the shared utility.

---

## Admin UI: `admin_map_layers.js`

The existing admin page manages `map_layers` rows. Add a **Page Configuration** sub-panel per layer:

- A table of existing `map_layer_page_config` rows for the selected layer — one row per page context it appears on.
- Columns: Page, Editable, Allow Create, Visible (default), Snap To This, Focus On Load — all inline-editable.
- **Add page** button — dropdown of known page contexts, creates a new row.
- **Remove** button — deletes the row (removes the layer from that page).

This replaces the need to edit JS files when adding a layer to a new page or changing its editability.

Known page context values to populate the dropdown:

```
projectedit, accountedit, stocklistedit, wayleaveedit, opportunityedit, map
```

---

## Data Migration

The junction table must be populated before the hardcoded arrays are removed. Reference and base layers are common to all pages — add a row for every page context for each of them. Vector layers are page-specific and listed below.

### `map` page context — seeded from existing `map_layers` columns

The existing `visible`, `layer_editable`, `layer_allow_create`, `snap_to_this`, `focus_on_load` values in `map_layers` represent the current `map_v5.js` configuration. These seed the junction rows for the `map` page context directly, preserving existing behaviour exactly:

```sql
INSERT INTO public.map_layer_page_config
    (layer_id, page_context, layer_editable, layer_allow_create, visible, snap_to_this, focus_on_load)
SELECT
    layer_id, 'map', layer_editable, layer_allow_create, visible, snap_to_this, focus_on_load
FROM public.map_layers
WHERE layer_active = true
ON CONFLICT DO NOTHING;
```

### Reference and base layers — all other page contexts

All layers with `layer_category IN ('reference', 'base')` get a row for each of the five non-map page contexts. `visible` defaults to `false` for reference layers and `true` for base layers to match current hardcoded behaviour; `layer_editable` and `layer_allow_create` are always `false` for these layer types.

```sql
-- Reference layers: visible = false on all pages
INSERT INTO public.map_layer_page_config
    (layer_id, page_context, layer_editable, layer_allow_create, visible)
SELECT l.layer_id, p.page_context, false, false, false
FROM public.map_layers l
CROSS JOIN (VALUES
    ('projectedit'), ('accountedit'), ('stocklistedit'), ('wayleaveedit'), ('opportunityedit')
) AS p(page_context)
WHERE l.layer_category = 'reference'
AND l.layer_active = true
ON CONFLICT DO NOTHING;

-- Base layers: visible per layer (sourced from map_layers.visible as a reasonable default)
INSERT INTO public.map_layer_page_config
    (layer_id, page_context, layer_editable, layer_allow_create, visible)
SELECT l.layer_id, p.page_context, false, false, l.visible
FROM public.map_layers l
CROSS JOIN (VALUES
    ('projectedit'), ('accountedit'), ('stocklistedit'), ('wayleaveedit'), ('opportunityedit')
) AS p(page_context)
WHERE l.layer_category = 'base'
AND l.layer_active = true
ON CONFLICT DO NOTHING;
```

### Vector layers — per page (requires manual review)

These are the layers with `layer_category` not in `reference`/`base`. Each entry below represents one `map_layer_page_config` row. Layer IDs reflect the **database** IDs (not the JS `layerID` values, which have known duplicates).

#### `projectedit` — 9 project layers

| Layer title | DB layer_id (to verify) | editable | allow_create | visible | snap |
|---|---|---|---|---|---|
| Project Boundary | 8 | true | false | true | false |
| Project Neighbours | 9 | false | false | true | false |
| Plan Network | 10 | false | false | true | false |
| Plan Equipment | 11 | true | true | true | true |
| Plan Structures | 19 | true | true | true | true |
| Plan Cables | 12 | true | true | true | true |
| Plan Duct | 17 | true | true | true | true |
| Plan SubDuct | 18 | true | true | true | true |
| PIA Blockages | (separate record — see note) | true | true | true | false |

> **Note:** The hardcoded JS uses layer_id `19` for both Plan Structures and PIA Blockages — a known duplicate. Verify actual DB IDs before populating the junction table. PIA Blockages likely has a different real ID.

#### `stocklistedit` — 2 layers

| Layer title | DB layer_id (to verify) | editable | allow_create | visible | snap |
|---|---|---|---|---|---|
| Stocklist Premises | (stocklist-specific record) | false | false | true | false |
| Project Boundary | 88 (separate read-only record) | false | false | true | false |

> **Note:** stocklist_edit.js uses layer_id `88` for Project Boundary (read-only view) vs `8` in projectedit (editable). With the new system, it is one row in `map_layers` with per-page config controlling editability — confirm which DB record is intended.

#### `accountedit` — 2 layers

| Layer title | DB layer_id (to verify) | editable | allow_create | visible | snap |
|---|---|---|---|---|---|
| Project Boundary | 8 | true | false | true | false |
| Stocklist Premises | 81 | false | false | true | false |

#### `opportunityedit` — 3 layers

| Layer title | DB layer_id (to verify) | editable | allow_create | visible | snap |
|---|---|---|---|---|---|
| Opportunity Equipment | (opportunity-specific record) | false | false | true | false |
| Project Boundary | 8 | true | false | true | false |
| Stocklist Premises | (stocklist-specific record) | false | false | true | false |

#### `wayleaveedit` — 5 layers (to be confirmed)

`wayleave_edit_v2.js` does not have a hardcoded layer array — it already calls the API. The 5 wayleave vector layers and their editability need to be specified and added as new records in `map_layers` (if not already present) before populating the junction table.

---

## Layer ID Audit (pre-migration prerequisite)

Before writing any junction table data, run the following query and reconcile against the hardcoded JS arrays:

```sql
SELECT layer_id, layer_title, layer_category, layer_active
FROM public.map_layers
ORDER BY layer_id;
```

For each hardcoded `layerID` value in the JS, confirm the matching DB record. Where the JS reuses the same ID for different layers on different pages (known cases: 19, 40, 43, 300, 431), determine which DB record is correct and whether a new record is needed.

---

## Database Changes

All database changes are delivered as `.sql` files in `db/` for manual execution. Files are named and sequenced so they can be run in order:

```
db/000_map_layers_corrections.sql      — new vector layer records + title corrections
db/001_map_layer_page_config.sql       — DDL: create junction table
db/002_map_layer_page_config_seed.sql  — data migration: map page context (from existing map_layers columns) + reference/base layers for all pages
db/003_map_layer_page_config_vector.sql — data migration: vector layers per page
db/004_map_layers_drop_columns.sql     — cleanup: drop redundant columns from map_layers (run after all pages verified in production)
```

### Column handling in `map_layers`

`public.map_layers` currently has columns that overlap with `map_layer_page_config`: `visible`, `layer_editable`, `layer_allow_create`, `snap_to_this`, `focus_on_load`. These are **not dropped immediately**. Instead:

1. Their existing values are used to seed `map_layer_page_config` rows for `page_context = 'map'` in `db/002_map_layer_page_config_seed.sql`, so `map_v5.js` behaviour is unchanged when it switches to `get-layers-for-page`.
2. The junction table is the authoritative source for all per-page config. The `get-layers-for-page` query selects from the junction table only — the `map_layers` columns are not used as a fallback.
3. `db/004_map_layers_drop_columns.sql` drops the five redundant columns from `map_layers` once all pages (including `map`) are live on the junction table and verified. This is a post-migration cleanup step, not part of the initial rollout.

---

## Build Order

### Phase 1 — Audits (no code or database changes)

1. **Layer ID audit** — query `map_layers`, reconcile against all five hardcoded JS arrays, document any missing or duplicate records. Output feeds into `db/000_map_layers_corrections.sql` and the vector layer tables in the Data Migration section above.

2. **Style audit** — compare `polyStyleManager`, `lineStyleManager`, `pointStyleManager` across `map_v5.js`, `project_edit_v2.js`, `opportunity_edit.js`, `stocklist_edit.js`, `account_edit_v2.js`. Document which category/type keys are shared, which differ, and which are file-unique. Save findings to `docs/map_style_audit.md` — listing every category, type key, and colour/style values per file so the pre-consolidation state is recoverable if needed.

### Phase 2 — Database

3. **Map layers corrections** — produce `db/000_map_layers_corrections.sql` from audit findings; execute to resolve any missing or duplicate records before proceeding.

4. **DDL** — produce and execute `db/001_map_layer_page_config.sql` to create the junction table.

5. **Data migration** — produce and execute `db/002_map_layer_page_config_seed.sql` (reference/base layers) and `db/003_map_layer_page_config_vector.sql` (vector layers per page, including wayleave once confirmed).

### Phase 3 — Backend

6. **Backend PHP** — add `get-layers-for-page` mode to `map_layer_manager.php`; add CRUD modes for admin.

### Phase 4 — JavaScript

7. **`map_layer_utils.js`** — create the shared utility file with: `loadLayersFromAPI()`, `mapApiResponseToLayerData()`, consolidated style managers, and `getPolyStyle()` / `getLineStyle()` / `getPointStyle()` resolver functions. Register it in `global_functions.php` for all map routes. Update `map_v5.js` to remove its local implementations and call `MapLayerUtils.*` instead. Verify the existing map page still works before proceeding.

8. **Port one page first** — `stocklistedit` is the lowest risk (only 2 vector layers, neither is complex). Validate the full flow end-to-end before touching `projectedit`.

9. **Port remaining pages** — `opportunityedit`, `accountedit`, `projectedit` (last, as it has the most vector layers and is the most complex). Each port removes the local style managers and replaces inline style resolution with `MapLayerUtils` calls.

10. **Update `wayleaveedit`** — replace `WL.fetchLayers()` with `MapLayerUtils.loadLayersFromAPI('wayleaveedit')`; extend `WL.createMapLayerControls()` to handle vector layer types using `MapLayerUtils` style lookups.

### Phase 5 — Admin & Cleanup

11. **Admin UI** — add Page Configuration sub-panel to `admin_map_layers.js` once all pages are live.

12. **Remove hardcoded arrays and dead code** — delete `const layersData = [...]`, local style managers, and inline style resolver functions from each JS file once its page is confirmed working in production. `wayleave_edit_v2.js` has no array to remove; `WL.fetchLayers()` and `WL._layersCache` can be deleted once step 10 is confirmed.

13. **Drop redundant columns from `map_layers`** — once all pages including `map` are confirmed working on the junction table, execute `db/004_map_layers_drop_columns.sql` to remove `visible`, `layer_editable`, `layer_allow_create`, `snap_to_this`, `focus_on_load` from `public.map_layers`. Do not run this until step 12 is complete and the system has been stable in production.

---

## Wayleave Vector Layers — Implementation Debt (Not an Architectural Constraint)

The five wayleave map layers are not part of the DB-driven layer pattern and are excluded from `db/000_map_layers_corrections.sql` and `db/003_map_layer_page_config_vector.sql`. This is implementation debt, not a technical necessity.

### What was built and why it diverged

The five entity layers are created as raw OL vector objects directly in `initMap()` inside `wayleave_edit_v2.js` (Map IIFE, ~line 1027), loaded in a single bundled call to `fn/wayleave_map_load.php`:

| Layer title | OL variable | Notes |
|---|---|---|
| Wayleave Boundary | `polyLayer` | Editable — agreement polygon drawing |
| UPRNs – boundary | `layerEntityPolygon` | Read-only |
| UPRNs – direct | `layerEntityDirect` | Read-only |
| UPRNs – stocklists | `layerEntityStocklist` | Read-only |
| UPRNs – titles | `layerEntityTitle` | Read-only |

When the wayleave module was built (April 2026), `project_edit_v2.js` also had a hardcoded `layersData` array — the DB-driven pattern didn't exist yet. The implementing agent bundled both datasets into one bespoke endpoint rather than following the per-layer URL pattern, and left a comment (*"A future unified map module will handle parity end-to-end"*) that does not describe what this migration is doing.

### Why it could have matched projects

Reading `fn/wayleave_map_load.php`:

- The **polygons** query is a trivial filtered SELECT on `wayleave.agreement_polygons` — structurally identical to any other vector layer. It could have been `map_get_v2.php?geotable=wayleave.agreement_polygons&geomfield=geom` with no further work.
- The **premises** query is a four-source CTE union with release status, which is more complex — but it is still a single SELECT that could be expressed as a DB view, then served the same way.

There is no architectural reason these layers are different from project vector layers. It was a shortcut taken at implementation time.

### What is needed to fix it

1. Create a DB view for each layer (e.g. `wayleave.vw_map_agreement_polygons`, `wayleave.vw_map_premises`) that accepts `agreement_id` filtering in the same way project views accept `project_id`.
2. Add five `map_layers` records pointing to those views via `map_get_v2.php`.
3. Add junction rows in `map_layer_page_config` for `page_context = 'wayleaveedit'`.
4. Replace the hardcoded `initMap()` layer creation and `WL.fetchLayers()` / `wayleave_map_load.php` calls with `MapLayerUtils.loadLayersFromAPI('wayleaveedit')`.

This is the correct path to parity with projectedit. It is deferred from the current migration but should be tracked as a follow-up, not treated as inherently different work.
