| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556 |
- <?php
- // generate_report.php (same-folder version)
- // Place alongside tas_spp_index.json
- ini_set('display_errors','0'); error_reporting(E_ALL);
- header('Content-Type: application/json; charset=utf-8');
- // ---------- Helpers ----------
- // Build a quick lookup map from an array of zones
- function build_zone_index(array $zones): array {
- $idx = [];
- foreach ($zones as $z) {
- $name = trim((string)($z['name'] ?? ''));
- if ($name === '') continue;
- $canon = $z; $canon['name'] = $name; // ensure name present
- $labels = [$name];
- if (!empty($z['short_name'])) $labels[] = $z['short_name'];
- if (!empty($z['aliases']) && is_array($z['aliases']))
- $labels = array_merge($labels, $z['aliases']);
- foreach ($labels as $lab) {
- $k = mb_strtolower(trim((string)$lab));
- if ($k !== '') $idx[$k] = $canon;
- }
- }
- return $idx;
- }
- // Build a quick lookup map from codes (object or array) + optional crosswalk
- function build_code_index(array $codes, array $crosswalkAliases = []): array {
- $idx = [];
- $add = function($label, $entry) use (&$idx) {
- $k = mb_strtolower(trim((string)$label));
- if ($k !== '') $idx[$k] = $entry;
- };
- // Dict-style (e.g., "C13": {...}) or array of objects
- $isAssoc = !empty($codes) && array_keys($codes) !== range(0, count($codes)-1);
- if ($isAssoc) {
- foreach ($codes as $key => $c) {
- $name = $c['name'] ?? $c['title'] ?? $key;
- $entry = $c; $entry['name'] = $name; $entry['id'] = $key;
- $add($key, $entry);
- $add($name, $entry);
- if (!empty($c['aliases']) && is_array($c['aliases']))
- foreach ($c['aliases'] as $al) $add($al, $entry);
- }
- } else {
- foreach ($codes as $c) {
- $name = $c['name'] ?? $c['title'] ?? '';
- if ($name === '') continue;
- $entry = $c; $entry['name'] = $name;
- $add($name, $entry);
- if (!empty($c['aliases']) && is_array($c['aliases']))
- foreach ($c['aliases'] as $al) $add($al, $entry);
- }
- }
- // Crosswalk (e.g., "bushfire" -> "C13")
- foreach ($crosswalkAliases as $alias => $target) {
- $targetKey = mb_strtolower(trim((string)$target));
- if (isset($idx[$targetKey])) $add($alias, $idx[$targetKey]);
- }
- return $idx;
- }
- function load_index(string $path) {
- if (!file_exists($path)) throw new RuntimeException("tas_spp_index.json not found");
- $raw = file_get_contents($path);
- // Strip BOM
- if (substr($raw,0,3) === "\xEF\xBB\xBF") $raw = substr($raw,3); // strip UTF-8 BOM
- $json = json_decode($raw, true);
- if (!is_array($json)) throw new RuntimeException("tas_spp_index.json is not valid JSON");
- return $json;
- }
- function spp_url(string $section) {
- static $tpso;
- if ($tpso === null) {
- $idx = json_decode(file_get_contents(__DIR__.'/tas_spp_index.json'), true);
- $tpso = $idx['tpso'] ?? [];
- }
- $id = $tpso['map'][$section] ?? null;
- $base = $tpso['base'] ?? 'https://tpso.planning.tas.gov.au/tpso/external/planning-scheme-viewer/30/section/';
- $eff = $tpso['effectiveForDate'] ?? '2025-08-27';
- return $id ? ($base . $id . '?effectiveForDate=' . rawurlencode($eff))
- : ($base . rawurlencode($section) . '?effectiveForDate=' . rawurlencode($eff));
- }
- function tpso_url_for(string $code, array $idx, ?string $fallbackType = null): ?string {
- $base = $idx['tpso']['base'] ?? 'https://tpso.planning.tas.gov.au/tpso/external/planning-scheme-viewer/30/section/';
- $eff = $idx['tpso']['effectiveForDate'] ?? '2025-08-27';
- $map = (array)($idx['tpso']['map'] ?? []);
- // Normalised key lookup (e.g. "8.1", "C13.6.2")
- $key = strtoupper(trim($code));
- if (isset($map[$key])) {
- return $base . $map[$key] . '?effectiveForDate=' . rawurlencode($eff);
- }
- // Fallback to an index page if provided
- $fbKey = $fallbackType === 'zone' ? 'ZONES_INDEX' :
- ($fallbackType === 'code' ? 'CODES_INDEX' : null);
- if ($fbKey && isset($map[$fbKey])) {
- return $base . $map[$fbKey] . '?effectiveForDate=' . rawurlencode($eff);
- }
- return null; // render plain text if we don't know the id yet
- }
- function render_clause_link(string $label, ?string $clauseCode, array $idx, ?string $fallbackType = null): string {
- if (!$clauseCode) return '';
- $url = tpso_url_for($clauseCode, $idx, $fallbackType);
- $labelEsc = htmlspecialchars($label);
- $codeEsc = htmlspecialchars($clauseCode);
- if ($url) {
- $urlEsc = htmlspecialchars($url);
- return "<li><strong>{$labelEsc}:</strong> <a href=\"{$urlEsc}\" target=\"_blank\" rel=\"noopener\">{$codeEsc}</a></li>";
- }
- // No mapping yet → show plain code (no broken link)
- return "<li><strong>{$labelEsc}:</strong> {$codeEsc}</li>";
- }
- // Normalise zone names so "General Residential" == "General Residential Zone"
- function norm_zone($s) {
- $s = trim(strtolower($s));
- $s = preg_replace('/\s+zone$/i','', $s);
- return $s;
- }
- // Fuzzy zone lookup against index (uses suffix stripping + contains matches)
- function find_zone($indexZones, $inputName) {
- $needle = norm_zone($inputName);
- foreach ($indexZones as $z) {
- if (empty($z['name'])) continue;
- if (norm_zone($z['name']) === $needle) return $z;
- }
- foreach ($indexZones as $z) {
- $n = norm_zone($z['name'] ?? '');
- if ($n !== '' && ($n === $needle || str_contains($n, $needle) || str_contains($needle, $n))) return $z;
- }
- return null;
- }
- // Map LIST overlay names to C-code ids (heuristics)
- function code_id_from_name($name) {
- $n = strtolower($name);
- if (str_contains($n,'bushfire')) return 'C13';
- if (str_contains($n,'heritage')) return 'C6';
- if (str_contains($n,'parking') || str_contains($n,'transport')) return 'C2';
- if (str_contains($n,'road') || str_contains($n,'rail')) return 'C3';
- if (str_contains($n,'telecom')) return 'C5';
- if (str_contains($n,'flood')) return 'C12';
- if (str_contains($n,'coastal inund')) return 'C11';
- if (str_contains($n,'coastal ero')) return 'C10';
- if (str_contains($n,'landslide') || str_contains($n,'landslip')) return 'C15';
- if (str_contains($n,'contamin')) return 'C14';
- if (str_contains($n,'electric') && str_contains($n,'trans')) return 'C4';
- if (str_contains($n,'scenic')) return 'C8';
- if (str_contains($n,'natural') || str_contains($n,'biodiv') || str_contains($n,'waterway') || str_contains($n,'coastal protection')) return 'C7';
- if (str_contains($n,'attenuation')) return 'C9';
- if (str_contains($n,'sign')) return 'C1';
- if (str_contains($n,'airport') || str_contains($n,'airfield')) return 'C16';
- return null; // unknown -> leave as plain text
- }
- try {
- // ---------- Input ----------
- $in = json_decode(file_get_contents('php://input'), true);
- if (!$in || empty($in['address']) || empty($in['planning_zones'])) {
- http_response_code(400);
- echo json_encode(['ok'=>false,'error'=>'Missing property payload']); exit;
- }
- // ---------- DEBUG ----------
- $debug = !empty($in['debug']);
- $log = [];
- $DLOG = function($m) use (&$log) {
- $s = is_string($m) ? $m : json_encode($m, JSON_UNESCAPED_SLASHES);
- $log[] = $s;
- error_log('[AIReport] '.$s);
- };
- $DLOG(['received_zones'=>$in['planning_zones'] ?? [], 'received_codes'=>$in['planning_codes'] ?? []]);
- // ---------- Load index ----------
- $indexPath = __DIR__ . '/tas_spp_index.json';
- if (!is_file($indexPath)) throw new RuntimeException('tas_spp_index.json not found');
- $idx = json_decode(file_get_contents($indexPath), true);
- if (!$idx) throw new RuntimeException('tas_spp_index.json is not valid JSON');
- $sppVersion = $idx['meta']['version'] ?? 'unknown';
- $zonesIndexArr = (array)($idx['zones'] ?? []);
- $codesIndex = (array)($idx['codes'] ?? []);
- $aliasMap = (array)($idx['crosswalk']['aliases'] ?? []);
- /* ---------- Names (as shown at top of report) ---------- */
- $zoneNames = array_values((array)$in['planning_zones']);
- $codeNames = array_values((array)$in['planning_codes']);
- /* ---------- Zone matching (fuzzy) ---------- */
- // map zones by case-insensitive name match against idx["zones"] (array of objects)
- $zoneBlobs = [];
- foreach ($zoneNames as $zn) {
- $match = find_zone($zonesIndexArr, $zn);
- if ($match) {
- $zoneBlobs[] = $match;
- $DLOG(['zone_match'=>$zn, 'matched_to'=>$match['name'] ?? null, 'doc_section'=>$match['doc_section'] ?? null]);
- } else {
- $zoneBlobs[] = ['name'=>$zn]; // fall back to plain name
- $DLOG(['zone_match'=>$zn, 'matched_to'=>null]);
- }
- }
- $codeBlobs = [];
- $alias = (array)($idx['crosswalk']['aliases'] ?? []);
- $codesIndex = (array)($idx['codes'] ?? []);
- foreach ($codeNames as $cn) {
- $matchKey = null;
- // try direct key like "C13"
- if (isset($codesIndex[$cn])) { $matchKey = $cn; }
- // try alias like "bushfire" -> "C13"
- else {
- foreach ($alias as $k=>$v) {
- if (strcasecmp($k, $cn)===0 || strcasecmp($v, $cn)===0) { $matchKey = $v; break; }
- }
- }
- $codeBlobs[] = $matchKey && isset($codesIndex[$matchKey])
- ? ['name'=>$matchKey] + $codesIndex[$matchKey]
- : ['name'=>$cn];
- }
- /* ---------- Code matching (heuristic) ---------- */
- // Build friendly code objects preserving the original label + inferred id (if any)
- $codeMatches = [];
- foreach ($codeNames as $label) {
- $id = null;
- // direct dictionary hit (e.g. "C13")
- if (isset($codesIndex[$label])) $id = $label;
- // alias map hit (e.g. "bushfire" -> "C13")
- if (!$id) {
- foreach ($aliasMap as $k=>$v) {
- if (strcasecmp($k, $label)===0 || strcasecmp($v, $label)===0) { $id = $v; break; }
- }
- }
- // heuristic from LIST label (e.g. "Safeguarding of Airports Code" -> "C16")
- if (!$id) {
- $heur = code_id_from_name($label);
- if ($heur && isset($codesIndex[$heur])) $id = $heur;
- }
- $def = $id && isset($codesIndex[$id]) ? $codesIndex[$id] : null;
- $codeMatches[] = ['id'=>$id, 'label'=>$label, 'def'=>$def];
- $DLOG(['code_match'=>$label, 'id'=>$id, 'has_key_clauses'=> $def && !empty($def['key_clauses'])]);
- }
- // ----- Intended-use rules (optional section) -----
- $intended = trim((string)($in['intended_use'] ?? ''));
- $intentRows = [];
- if ($intended !== '') {
- // Load rules
- $rulesPath = __DIR__ . '/tas_use_rules.json';
- $useRules = is_file($rulesPath) ? json_decode(file_get_contents($rulesPath), true) : null;
- if (is_array($useRules)) {
- // Normalise the user's intended use via aliases
- $canonUse = strtolower($intended);
- foreach (($useRules['aliases'] ?? []) as $k => $arr) {
- if (strtolower($k) === $canonUse) { $canonUse = $k; break; }
- foreach ((array)$arr as $alt) {
- if (strtolower($alt) === strtolower($intended)) { $canonUse = $k; break 2; }
- }
- }
- foreach ($zoneBlobs as $z) {
- $zname = $z['name'] ?? '';
- $zrules = $useRules['rules'][$zname] ?? [];
- $match = null;
- foreach ($zrules as $r) {
- if (strtolower($r['use'] ?? '') === $canonUse) { $match = $r; break; }
- }
- // Build a row per zone
- $useTable = $z['key_clauses']['use_table'] ?? ($match['refs']['use_table'] ?? null);
- $standards = $match['refs']['standards'] ?? [];
- $statusCode = $match['status'] ?? 'CHECK';
- $statusText = $useRules['enums']['status'][$statusCode] ?? 'Check Use Table';
- $intentRows[] = [
- 'zone' => $zname ?: 'Zone',
- 'status' => $statusText,
- 'use_table' => $useTable,
- 'standards' => $standards,
- 'notes' => $match['notes'] ?? null
- ];
- }
- }
- }
- /* ---------- Small trimmed RAW sample for future LLM ---------- */
- // ---- Prepare RAW (optional) for LLM context; trim to keep prompts lean ----
- $raw = $in['raw'] ?? null;
- // pick first feature’s attrs for each layer, keep up to ~40 keys to avoid giant prompts
- $pickFirst = function($arr){
- $a = (array)$arr;
- if (isset($a[0]) && is_array($a[0])) {
- $attrs = $a[0];
- return array_slice($attrs, 0, 40, true);
- }
- return null;
- };
- $raw_for_prompt = [
- 'parcel_attrs' => $pickFirst($raw['parcels'] ?? []),
- 'lga_attrs' => $pickFirst($raw['lga'] ?? []),
- 'zone_attrs' => $pickFirst($raw['zones'] ?? []),
- 'code_attrs' => $pickFirst($raw['codes'] ?? []),
- ];
- // ---- Build a concise, LLM-friendly context blob (you can expand later) ----
- $context = [
- 'address' => (string)$in['address'],
- 'pid' => $in['pid'] ?? null,
- 'title_id' => $in['title_id'] ?? null,
- 'council' => $in['council'] ?? null,
- 'planning_scheme' => $in['planning_scheme'] ?? null,
- 'zones' => array_map(fn($z)=>[
- 'name' => $z['name'] ?? null,
- 'doc_section' => $z['doc_section'] ?? null,
- 'key_clauses' => $z['key_clauses'] ?? null], $zoneBlobs),
- 'codes' => array_map(fn($c)=>[
- 'id' => $c['id'] ?? null,
- 'name' => $c['name'] ?? null,
- 'key_clauses' => $c['key_clauses'] ?? null], $codeMatches),
- 'area' => [
- 'sqm' => $in['total_area']['sqm'] ?? null,
- 'sqm_label' => $in['total_area']['sqm_label'] ?? null,
- 'ha' => $in['total_area']['ha'] ?? null,
- 'ha_label' => $in['total_area']['ha_label'] ?? null,
- ],
- 'spp_version' => $sppVersion,
- 'raw_sample' => $raw_for_prompt, // safe, trimmed
- ];
- // ====== (optional) Hook an LLM here ======
- // Leave disabled by default. When ready, implement call_llm() to your provider.
- $USE_LLM = false;
- $llm_html = null;
- if ($USE_LLM) {
- $prompt = [
- "role" => "user",
- "content" =>
- "You are an expert planning assistant for the Tasmanian Planning Scheme (SPPs). Please don't make anything up, strictly stick to the wording and context of the State Planning Provisions.".
- "Using the JSON context below, write a concise property planning summary: ".
- "1) Identify applicable zones and codes (with clause references from key_clauses). ".
- "2) For an *intended use* placeholder (like dwelling, shed, café), outline likely status (Permitted/Discretionary/Prohibited) and key standards to check. ".
- "3) Include hyperlinks to the relevant SPP sections when doc_section is available (base URL: https://tpso.planning.tas.gov.au/tpso/external/planning-scheme-viewer/30/section/<DOC>?effectiveForDate={$sppVersion}). ".
- "4) Keep it practical, note uncertainty and that LPS may vary locally.\n\n".
- "Context JSON:\n".
- json_encode($context, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)
- ];
- // $llm_html = call_llm([$prompt]); // TODO: implement your provider call
- }
- /* ---------- Map image (accept data URL or raw base64) ---------- */
- $mapSrc = null;
- if (!empty($in['map_png'])) {
- $mapSrc = (stripos($in['map_png'], 'data:image') === 0)
- ? $in['map_png']
- : ('data:image/png;base64,' . $in['map_png']);
- }
- // ---------- HTML (template) ----------
- ob_start(); ?>
- <div>
- <h3 class="mb-2">AI-Assisted Property Report (Tasmania)</h3>
- <div class="text-muted mb-3">
- Generated <?= htmlspecialchars(gmdate('Y-m-d H:i')) ?> · SPP version: <?= htmlspecialchars($sppVersion) ?>
- </div>
- <?php if ($mapSrc): ?>
- <div class="mb-3">
- <img src="<?= htmlspecialchars($mapSrc) ?>" alt="Property map" style="max-width:100%;border-radius:.5rem;border:1px solid rgba(0,0,0,.1)" />
- </div>
- <?php endif; ?>
- <h5 class="mb-1"><?= htmlspecialchars($in['address']) ?></h5>
- <ul class="list-unstyled mb-3">
- <?php if (!empty($in['pid'])): ?><li><strong>PID:</strong> <?= htmlspecialchars($in['pid']) ?></li><?php endif; ?>
- <?php if (!empty($in['title_id'])): ?><li><strong>Title:</strong> <?= htmlspecialchars($in['title_id']) ?></li><?php endif; ?>
- <?php if (!empty($in['council'])): ?><li><strong>Council (LGA):</strong> <?= htmlspecialchars($in['council']) ?></li><?php endif; ?>
- <?php if (!empty($in['total_area']['sqm_label'])): ?>
- <li><strong>Area:</strong> <?= htmlspecialchars($in['total_area']['sqm_label']) ?>
- <?php if (!empty($in['total_area']['ha_label'])): ?> (<?= htmlspecialchars($in['total_area']['ha_label']) ?>)<?php endif; ?>
- </li>
- <?php endif; ?>
- </ul>
- <h6>Primary zone & overlays</h6>
- <p>
- <strong>Zone(s):</strong>
- <?= htmlspecialchars(implode(', ', $zoneNames)) ?: '—' ?><br/>
- <strong>Overlays/Codes:</strong>
- <?= htmlspecialchars(implode(', ', $codeNames)) ?: '—' ?>
- </p>
- <h6>Key scheme parts to review</h6>
- <ul>
- <?php foreach ($zoneBlobs as $z): ?>
- <li>
- <strong><?= htmlspecialchars($z['name'] ?? 'Zone') ?>:</strong>
- <?php if (!empty($z['key_clauses'])): ?>
- <ul class="mb-1">
- <?= render_clause_link('Purpose', $z['key_clauses']['purpose'] ?? null, $idx, 'zone') ?>
- <?= render_clause_link('Use table', $z['key_clauses']['use_table'] ?? null, $idx, 'zone') ?>
- <?= render_clause_link('Use standards', $z['key_clauses']['use_standards'] ?? null, $idx, 'zone') ?>
- <?= render_clause_link('Development standards — dwellings', $z['key_clauses']['dev_standards_dwellings'] ?? null, $idx, 'zone') ?>
- <?= render_clause_link('Development standards — buildings & works', $z['key_clauses']['dev_standards_buildings_works'] ?? null, $idx, 'zone') ?>
- <?= render_clause_link('Subdivision', $z['key_clauses']['subdivision'] ?? null, $idx, 'zone') ?>
- </ul>
- <?php if (!empty($z['doc_section'])): $zUrl = tpso_url_for($z['doc_section'], $idx, 'zone'); if ($zUrl): ?>
- <div class="small">Full zone chapter: <a href="<?= htmlspecialchars($zUrl) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($z['doc_section']) ?></a></div>
- <?php else: ?>
- <div class="small">Full zone chapter: <?= htmlspecialchars($z['doc_section']) ?></div>
- <?php endif; endif; ?>
- <?php else: ?>
- Refer to the State Planning Provisions zone chapter.
- <?php endif; ?>
- </li>
- <?php endforeach; ?>
- <?php foreach ($codeMatches as $cm): ?>
- <li>
- <strong><?= htmlspecialchars($cm['label']) ?><?= $cm['id'] ? ' ('.$cm['id'].')' : '' ?>:</strong>
- <?php if ($cm['def'] && !empty($cm['def']['key_clauses'])): $kc = $cm['def']['key_clauses']; ?>
- <ul class="mb-1">
- <?= render_clause_link('Purpose', $kc['purpose'] ?? null, $idx, 'code') ?>
- <?= render_clause_link('Application', $kc['application'] ?? null, $idx, 'code') ?>
- <?= render_clause_link('Definitions', $kc['definitions'] ?? null, $idx, 'code') ?>
- <?= render_clause_link('Exempt development / use', ($kc['exempt_development'] ?? $kc['exempt_use'] ?? null), $idx, 'code') ?>
- <?= render_clause_link('Use standards', $kc['use_standards'] ?? null, $idx, 'code') ?>
- <?php if (!empty($kc['use_standards_detail']) && is_array($kc['use_standards_detail'])): ?>
- <?php foreach ($kc['use_standards_detail'] as $it):
- $id = $it['id'] ?? null;
- if ($id) {
- $u = tpso_url_for($id, $idx, 'code');
- if ($u) {
- echo '<li><a href="'.htmlspecialchars($u).'" target="_blank" rel="noopener">'.htmlspecialchars($id).'</a>'
- .($title ? ' – '.htmlspecialchars($title) : '')
- .'</li>';
- } else {
- echo '<li>'.htmlspecialchars($id).($title ? ' – '.htmlspecialchars($title) : '').'</li>';
- }
- }
- continue; ?>
- <li>
- <a href="<?= htmlspecialchars(spp_url($id)) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($id) ?></a>
- <?= $title ? ' – '.htmlspecialchars($title) : '' ?>
- </li>
- <?php endforeach; ?>
- <?php endif; ?>
- <?= render_clause_link('Development standards', $kc['development_standards'] ?? null, $idx, 'code') ?>
- <?php if (!empty($kc['development_standards_detail']) && is_array($kc['development_standards_detail'])): ?>
- <?php foreach ($kc['development_standards_detail'] as $it):
- $id = $it['id'] ?? null;
- if ($id) {
- $u = tpso_url_for($id, $idx, 'code');
- if ($u) {
- echo '<li><a href="'.htmlspecialchars($u).'" target="_blank" rel="noopener">'.htmlspecialchars($id).'</a>'
- .($title ? ' – '.htmlspecialchars($title) : '')
- .'</li>';
- } else {
- echo '<li>'.htmlspecialchars($id).($title ? ' – '.htmlspecialchars($title) : '').'</li>';
- }
- }
- continue; ?>
- <li>
- <a href="<?= htmlspecialchars(spp_url($id)) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($id) ?></a>
- <?= $title ? ' – '.htmlspecialchars($title) : '' ?>
- </li>
- <?php endforeach; ?>
- <?php endif; ?>
- </ul>
- <?php else: ?>
- Review the relevant SPP code chapter for triggers and standards.
- <?php endif; ?>
- </li>
- <?php endforeach; ?>
- </ul>
- <?php if ($intended !== ''): ?>
- <h6>Intended use (beta): <?= htmlspecialchars($intended) ?></h6>
- <ul class="mb-3">
- <?php foreach ($intentRows as $row): ?>
- <li>
- <strong><?= htmlspecialchars($row['zone']) ?>:</strong>
- <?= htmlspecialchars($row['status']) ?> —
- <?php if (!empty($row['use_table'])): ?>
- <a href="<?= htmlspecialchars(spp_url($row['use_table'])) ?>" target="_blank" rel="noopener">
- Use Table (<?= htmlspecialchars($row['use_table']) ?>)
- </a>
- <?php else: ?>Use Table<?php endif; ?>
- <?php if (!empty($row['standards'])): ?>
- · standards
- <?php foreach ($row['standards'] as $i => $st): ?>
- <a href="<?= htmlspecialchars(spp_url($st)) ?>" target="_blank" rel="noopener"><?= htmlspecialchars($st) ?></a><?= $i < count($row['standards']) - 1 ? ', ' : '' ?>
- <?php endforeach; ?>
- <?php endif; ?>
- <?php if (!empty($row['notes'])): ?> · <?= htmlspecialchars($row['notes']) ?><?php endif; ?>
- </li>
- <?php endforeach; ?>
- </ul>
- <?php endif; ?>
- <h6>Typical constraints to check</h6>
- <ul>
- <li>Use Table: whether your intended use is Permitted, Discretionary, or Prohibited.</li>
- <li>Zone development standards: height, setbacks, site coverage, privacy, access and parking.</li>
- <li>Code triggers (if mapped): bushfire, heritage, inundation, landslide, biodiversity, scenic/attenuation, coastal, roads/rail, parking.</li>
- <li>Local Provisions Schedule (<?= htmlspecialchars($in['council'] ?? 'Council') ?>): any local variations, Specific Area Plans, heritage list entries.</li>
- </ul>
- <h6>Next steps</h6>
- <ol>
- <li>Confirm zone and all overlays using the LIST map and <?= htmlspecialchars($in['council'] ?? 'Council') ?> LPS.</li>
- <li>Check your intended use against the zone Use Table; note any qualifications.</li>
- <li>Review development and code standards; obtain specialist reports where required.</li>
- <li>Seek pre-application advice from <?= htmlspecialchars($in['council'] ?? 'the local Council') ?> planning.</li>
- </ol>
- <div class="text-muted small mt-3">Disclaimer: This report summarises public datasets and references the Tasmanian Planning Scheme (SPPs). It is not legal advice. Verify all controls with the responsible authority.</div>
- </div>
- <?php if ($llm_html): ?>
- <div class="mt-3"><?= $llm_html ?></div>
- <?php endif; ?>
- <div class="mt-3">
- <?php if (!empty($in['debug'])): ?>
- <details class="mt-3"><summary>Debug context</summary><pre class="small" style="white-space:pre-wrap"><?= htmlspecialchars(json_encode($context, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES)) ?></pre></details>
- <?php endif; ?>
- </div>
- <?php
- $html = ob_get_clean();
- echo json_encode(['ok'=>true,'html'=>$html], JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
- } catch (Throwable $e) {
- http_response_code(500);
- echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
- }
- ?>
|