# SandTable Settings Architecture

## Overview

The SandTable system uses a settings-based architecture for remote control and state management. Settings are exposed via WebRTC/HTTP routes that can be accessed by remote controllers (like SandTableController) or external applications.

## Architecture Concepts

### Settings vs Commands

- **Settings**: Persistent state that can be read (GET) and written (PUT/POST). Settings represent the current configuration of the SandTable and are typically saved to localStorage.
  - Examples: `terrain3D`, `terrainExaggeration`, `pitch`, `bearing`, `roll`, `uiVisible`

- **Commands**: One-time actions that don't represent persistent state. Commands execute an operation but don't have a retrievable "current value."
  - Examples: `moveToLocation`, `captureScreenshot`, `resetView`

### Communication Flow

```
┌─────────────────────┐          WebRTC/HTTP          ┌─────────────────────┐
│  SandTableController│ ◄──────────────────────────► │     SandTable       │
│   (Remote Control)  │      PUT /settings/pitch     │   (Display Map)     │
│                     │      GET /settings/pitch     │                     │
└─────────────────────┘                               └─────────────────────┘
         │                                                      │
         │                                                      │
         ▼                                                      ▼
  acequia-integration.js                            acequia-integration.js
  - applySetting()                                  - registerRoute()
  - fetchSetting()                                  - parseRequestBody()
                                                    - createResponse()
                                                    - dispatchEvent()
```

## Components

### 1. SandTable (Server Side)

**File**: `/acequia/localhost/SandTable/src/modules/acequia-integration.js`

The SandTable exposes settings through HTTP/WebRTC routes registered with the Acequia framework.

#### Key Methods:
- `registerRoutes()`: Registers all settings and command endpoints
- `parseRequestBody(data, emoji)`: Parses incoming JSON request bodies
- `createResponse(status, contentType, body)`: Creates HTTP responses
- `addCorsHeaders(options)`: Adds CORS headers for cross-origin requests

### 2. SandTableController (Client Side)

**File**: `/acequia/localhost/SandTableController/src/modules/acequia-integration.js`

The controller consumes settings through HTTP/WebRTC requests to connected SandTables.

#### Key Methods:
- `applySetting(settingName, value)`: Sets a setting value on the connected table
- `fetchSetting(settingName)`: Gets a setting value from the connected table
- `subscribeToWebhook(path, callback)`: Subscribes to setting change notifications

### 3. Map Manager

**File**: `/acequia/localhost/SandTable/src/modules/map-manager.js`

Manages the map state and applies changes triggered by settings.

## Implementation Guide

### Adding a New Setting

Follow these steps to add a new setting to the SandTable system:

#### Step 1: Register the Route (SandTable)

In `SandTable/src/modules/acequia-integration.js`, add a new route in the `registerRoutes()` method:

```javascript
// Register /settings/yourSetting route
this.group.registerRoute(
  `/${instanceId}/settings/yourSetting`,
  async (data) => {
    const method = data.method || 'GET'
    console.log(`⚙️ HTTP ${method} yourSetting request:`, data)

    try {
      // Handle OPTIONS for CORS
      if (method === 'OPTIONS') {
        return {
          headers: this.addCorsHeaders({ contentType: 'text/plain' }),
          status: 200,
          body: '',
        }
      }

      // Handle PUT - set the setting
      if (method === 'PUT') {
        let yourSetting
        try {
          yourSetting = this.parseRequestBody(data, '⚙️')
          console.log('⚙️ Parsed yourSetting:', yourSetting, 'type:', typeof yourSetting)
        } catch (parseError) {
          console.error('⚙️ Parse error:', parseError)
          return this.createResponse(400, 'application/json', {
            error: 'Invalid JSON in request body',
            message: parseError.message,
          })
        }

        // Validate the value format
        if (typeof yourSetting !== 'object' || typeof yourSetting.value !== 'number') {
          return this.createResponse(400, 'application/json', {
            error: 'Invalid value format',
            message: 'Expected {value: number}',
          })
        }

        // Validate the value range (if applicable)
        if (yourSetting.value < 0 || yourSetting.value > 100) {
          return this.createResponse(400, 'application/json', {
            error: 'Value out of range',
            message: 'Value must be between 0 and 100',
          })
        }

        // Dispatch DOM event to trigger the action
        console.log('⚙️ Request to set yourSetting:', yourSetting.value)
        document.dispatchEvent(
          new CustomEvent('set-your-setting', {
            detail: { value: yourSetting.value },
            bubbles: true,
          })
        )

        return this.createResponse(200, 'application/json', {
          success: true,
          yourSetting: yourSetting.value,
          timestamp: new Date().toISOString(),
        })
      }

      // Handle GET - return current value
      const currentValue = this.mapManager?.yourSettingProperty || 0
      console.log('⚙️ GET yourSetting request - returning:', currentValue)
      return this.createResponse(200, 'application/json', currentValue)
    } catch (error) {
      console.error('Error handling yourSetting:', error)
      return this.createResponse(500, 'application/json', {
        error: 'Failed to handle yourSetting',
        message: error.message,
      })
    }
  }
)
```

#### Step 2: Handle the Event (SandTable)

In `SandTable/src/modules/map-manager.js`, add a property and event listener:

```javascript
// In constructor
constructor(options = {}) {
  // ... other properties
  this.yourSettingProperty = 50 // default value
}

// In setupEventListeners() or similar
setupEventListeners() {
  // ... other listeners

  // Listen for setting changes
  document.addEventListener('set-your-setting', (e) => {
    console.log('⚙️ Received set-your-setting event:', e.detail)
    this.setYourSetting(e.detail.value)
  })
}

// Add the method to apply the setting
setYourSetting(value) {
  console.log('⚙️ Setting yourSetting to:', value)
  this.yourSettingProperty = value

  // Apply the setting to the map or other components
  this.map?.doSomething(value)

  // Save to localStorage if needed
  this.saveLayerState()
}
```

#### Step 3: Add Controller Methods (SandTableController)

In `SandTableController/src/modules/acequia-integration.js`, add methods to interact with the setting:

```javascript
async setYourSetting(value) {
  console.log('⚙️ Setting yourSetting to:', value)
  return this.applySetting('yourSetting', value)
}

async getYourSetting() {
  console.log('⚙️ Getting yourSetting')
  return this.fetchSetting('yourSetting')
}
```

#### Step 4: Add UI Controls (SandTableController)

In `SandTableController/index.html`, add UI elements:

```html
<div class="control-row">
  <label class="label">Your Setting</label>
  <div class="slider-control">
    <input type="range" id="yourSettingSlider" class="slider" min="0" max="100" value="50" step="1">
    <span class="slider-value" id="yourSettingValue" style="min-width: 4ch;">50</span>
  </div>
</div>
```

In `SandTableController/src/main.js`, add event handlers:

```javascript
// Add slider event handlers
const yourSettingSlider = document.getElementById('yourSettingSlider')
const yourSettingValue = document.getElementById('yourSettingValue')

yourSettingSlider?.addEventListener('input', (e) => {
  const value = parseInt(e.target.value)
  yourSettingValue.textContent = value.toString()
})

yourSettingSlider?.addEventListener('change', async (e) => {
  const value = parseInt(e.target.value)
  try {
    await this.acequiaIntegration?.setYourSetting(value)
  } catch (error) {
    console.error('Failed to set yourSetting:', error)
    this.showStatus('Failed to set yourSetting', 'error')
  }
})

// Handle updates from the table
handleTableSettingsUpdate(propertyName, value) {
  // ... other cases

  case 'yourSetting':
    const yourSettingSlider = document.getElementById('yourSettingSlider')
    const yourSettingValue = document.getElementById('yourSettingValue')
    if (yourSettingSlider && typeof value === 'number') {
      yourSettingSlider.value = value
      if (yourSettingValue) {
        yourSettingValue.textContent = value.toString()
      }
    }
    break
}
```

#### Step 5: Persist State (Optional)

If the setting should persist across page reloads, add it to the layer state:

In `SandTable/src/modules/map-manager.js`:

```javascript
saveLayerState() {
  // ... other code

  const layerState = {
    // ... other properties
    yourSetting: this.yourSettingProperty,
  }
  this.storageManager.setLocalItem('sandTableLayerState', layerState)
}

loadLayerState() {
  // ... other code

  if (layerState.yourSetting !== undefined) {
    this.yourSettingProperty = layerState.yourSetting
  }
}
```

## Common Patterns

### Boolean Settings

Boolean settings use `{value: true/false}` format:

```javascript
// SandTable route
if (typeof setting !== 'object' || !(setting.value === true || setting.value === false)) {
  return this.createResponse(400, 'application/json', {
    error: 'Invalid value - must be boolean'
  })
}

// Controller
async toggleFeature(enabled) {
  return this.applySetting('featureName', enabled)
}
```

### Numeric Settings with Range

Numeric settings use `{value: number}` format:

```javascript
// SandTable route - validation
if (typeof setting !== 'object' || typeof setting.value !== 'number') {
  return this.createResponse(400, 'application/json', {
    error: 'Invalid value format - expected {value: number}'
  })
}

if (setting.value < minValue || setting.value > maxValue) {
  return this.createResponse(400, 'application/json', {
    error: `Value must be between ${minValue} and ${maxValue}`
  })
}
```

### Angle Settings (Degrees)

Camera orientation settings (pitch, bearing, roll):

```javascript
// Bearing: 0-360 degrees
// Pitch: 0-85 degrees
// Roll: -180 to 180 degrees (or 0-360)

this.map.easeTo({
  bearing: value,
  pitch: value,
  roll: value,
  duration: 500
})
```

### Settings Without getRoll-style Methods

Some MapLibre properties don't have getter methods. Track them internally:

```javascript
// In constructor
this.currentRoll = 0

// When setting
setCameraRoll(roll) {
  this.currentRoll = roll  // Store internally
  this.map.easeTo({ roll: roll, duration: 500 })
}

// When reading
const roll = this.currentRoll  // Read from internal property
```

## Webhook System

The SandTable can notify controllers when settings change using webhooks.

### Registering a Webhook (SandTable)

```javascript
// In acequia-integration.js
this.group.registerRoute(
  `/${instanceId}/webhooks/settings`,
  async (data) => {
    const method = data.method || 'POST'

    if (method === 'POST') {
      const webhook = this.parseRequestBody(data, '🔔')

      // Store webhook subscriber
      const subscriberId = `${Date.now()}-${Math.random()}`
      this.webhookSubscribers.set(subscriberId, {
        path: webhook.path,
        callback: webhook.callback,
        filters: webhook.filters || {}
      })

      return this.createResponse(200, 'application/json', {
        success: true,
        subscriberId: subscriberId
      })
    }
  }
)
```

### Subscribing to Webhooks (Controller)

```javascript
// Subscribe to specific setting changes
await this.acequiaIntegration.subscribeToWebhook('/settings/pitch', (value) => {
  console.log('Pitch changed to:', value)
  this.handleTableSettingsUpdate('pitch', value)
})
```

## Testing Settings

### Manual Testing

1. Open browser console on both SandTable and Controller
2. Watch for log messages with emojis (🏔️, ⚙️, etc.)
3. Use controller UI to change settings
4. Verify logs show:
   - Controller sending PUT request
   - SandTable receiving and parsing request
   - Event dispatched
   - Setting applied
   - Response sent back

### Console Testing

From browser console:

```javascript
// On SandTable - trigger setting change
document.dispatchEvent(new CustomEvent('set-your-setting', {
  detail: { value: 75 },
  bubbles: true
}))

// On Controller - send setting
await acequiaIntegration.applySetting('yourSetting', 75)

// On Controller - get setting
const value = await acequiaIntegration.fetchSetting('yourSetting')
console.log('Current value:', value)
```

## Best Practices

1. **Always validate input**: Check type, format, and range
2. **Use descriptive emojis**: Makes console logs easier to scan
3. **Log liberally**: Log at every step (receive, parse, validate, apply, respond)
4. **Handle errors gracefully**: Return proper HTTP status codes and error messages
5. **Use DOM events**: Decouple route handlers from map logic
6. **Persist important state**: Save to localStorage via `saveLayerState()`
7. **Support GET requests**: Allow controllers to query current state
8. **Add CORS headers**: Enable cross-origin requests
9. **Use consistent naming**: Match setting names across all components
10. **Document value formats**: Clearly specify expected JSON structure

## Troubleshooting

### Setting not updating

1. Check browser console for error messages
2. Verify route is registered correctly
3. Verify event listener is attached
4. Check if value validation is failing
5. Verify DOM event is being dispatched

### Controller shows stale values

1. Implement webhook subscription
2. Verify GET endpoint returns current value
3. Check if controller is polling for updates
4. Verify `handleTableSettingsUpdate()` is called

### Setting not persisting

1. Add setting to `saveLayerState()`
2. Add setting to `loadLayerState()`
3. Verify localStorage is working
4. Check if `isLoadingState` flag is blocking saves

### CORS errors

1. Verify `addCorsHeaders()` is called
2. Handle OPTIONS requests
3. Check server configuration
4. Verify request origin matches allowed origins

## Examples

See the following implementations for reference:
- `/settings/terrain3D` - Boolean toggle
- `/settings/terrainExaggeration` - Numeric range (0.1 - 5)
- `/settings/pitch` - Camera angle (0 - 85°)
- `/settings/bearing` - Camera rotation (0 - 360°)
- `/settings/roll` - Camera roll with internal tracking
- `/settings/uiVisible` - UI toggle with DOM manipulation
