false, 'error' => 'Use POST with application/json']); exit; } // Reject non-JSON content types early — prevents json_decode silently // returning null on form-encoded or multipart bodies. $ct = $_SERVER['CONTENT_TYPE'] ?? ''; if (strpos($ct, 'application/json') === false) { http_response_code(415); echo json_encode(['ok' => false, 'error' => 'Content-Type must be application/json']); exit; } $raw = file_get_contents('php://input') ?: ''; $in = json_decode($raw, true, 512, JSON_THROW_ON_ERROR); // --------- Schema (minimum viable) ---------- $d = [ // site 'address' => trim((string)($in['address'] ?? '')), 'lat' => floatval($in['lat'] ?? 0), 'lng' => floatval($in['lng'] ?? 0), 'pid' => trim((string)($in['pid'] ?? '')), 'title_id' => trim((string)($in['title_id'] ?? '')), 'total_area' => $in['total_area'] ?? null, // e.g. {sqm_label:"1,652 m²", ha_label:"0.1652 ha"} or string 'area_sqm' => $in['area_sqm'] ?? '', 'area_ha' => $in['area_ha'] ?? '', 'tenure' => trim((string)($in['tenure'] ?? '')), 'lpi' => trim((string)($in['lpi'] ?? '')), 'list_guid' => trim((string)($in['list_guid'] ?? '')), 'locality' => trim((string)($in['locality'] ?? '')), 'council' => trim((string)($in['council'] ?? '')), 'planning_scheme' => trim((string)($in['planning_scheme'] ?? 'Tasmanian Planning Scheme')), 'planning_zones' => array_values(array_filter((array)($in['planning_zones'] ?? []))), 'planning_codes' => array_values(array_filter((array)($in['planning_codes'] ?? []))), // proposal 'use_class' => trim((string)($in['use_class'] ?? 'Educational and Occasional Care')), 'proposal_summary' => trim((string)($in['proposal_summary'] ?? '')), // 1–3 paras freeform 'operations' => (array)($in['operations'] ?? []), // ['hours','staff','children'] 'parking' => (array)($in['parking'] ?? []), // ['cars','bikes','accessible','motorcycle'] 'signage' => (array)($in['signage'] ?? []), // [{type,desc}, ...] 'consultants' => (array)($in['consultants'] ?? []), // ['TIA'=>'…','Acoustic'=>'…','Bushfire'=>'…'] // overlays 'overlays' => (array)($in['overlays'] ?? []), // ['bushfire'=>bool, 'airport_noise'=>bool, ...] // assessments matrix (zone + codes) 'standards' => (array)($in['standards'] ?? []), // [{clause, standard, acceptable, relies_on_pc:[], notes}] // assets (optional) 'map_png' => (string)($in['map_png'] ?? ''), // data:image/png;base64,... 'appendices' => array_values(array_filter((array)($in['appendices'] ?? []))), // meta 'prepared_for' => trim((string)($in['prepared_for'] ?? '')), 'prepared_by' => trim((string)($in['prepared_by'] ?? 'Modulos Design')), 'author' => trim((string)($in['author'] ?? '')), 'job_number' => trim((string)($in['job_number'] ?? '')), 'version' => trim((string)($in['version'] ?? 'Draft')), 'prepared_date' => trim((string)($in['prepared_date'] ?? date('j F Y'))), ]; // Minimal required fields foreach (['address','planning_scheme'] as $k) { if ($d[$k] === '' || $d[$k] === null) { throw new RuntimeException("Missing required field: {$k}"); } } // Helpers ----------------------------------------------------------- $join = fn(array $arr, string $sep=', ') => implode($sep, array_values(array_filter($arr, fn($x)=>$x!=='' && $x!==null))); $fmt_area = function($d) { if (is_array($d['total_area'] ?? null)) { return $d['total_area']['ha_label'] ?? $d['total_area']['sqm_label'] ?? ''; } return $d['area_ha'] ?: $d['area_sqm']; }; $md_table_row = fn(array $cols) => '| ' . implode(' | ', array_map(fn($c)=>trim((string)$c) ?: '–', $cols)) . ' |'; $render_standards = function(array $rows) use ($md_table_row) { if (!$rows) return "_No specific standards provided in this draft._\n"; $out = []; $out[] = $md_table_row(['Clause','Standard','AS','PC','Notes']); $out[] = $md_table_row(['---','---','---','---','---']); foreach ($rows as $r) { $out[] = $md_table_row([ $r['clause'] ?? '', $r['standard'] ?? '', $r['acceptable'] ?? '', $r['relies_on_pc'] ? implode(', ', (array)$r['relies_on_pc']) : '', $r['notes'] ?? '' ]); } return implode("\n", $out)."\n"; }; // Markdown builder -------------------------------------------------- $md = []; $md[] = '# Supporting Planning Report'; $md[] = ''; $md[] = "Prepared for: **" . ($d['prepared_for'] ?: '—') . "**"; $md[] = "Prepared by: **" . ($d['prepared_by'] ?: '—') . "**" . ($d['author'] ? " ({$d['author']})" : ""); $md[] = "Job No.: **" . ($d['job_number'] ?: '—') . "**"; $md[] = "Version: **" . ($d['version'] ?: '—') . "**"; $md[] = "Date: **" . ($d['prepared_date'] ?: '—') . "**"; $md[] = ''; $md[] = '> ' . $d['acknowledgement']; $md[] = ''; // Permit overview $md[] = '## Permit overview'; $md[] = '### Permit application details'; $md[] = $md_table_row(['Applicant','Owner','Address','Title']); $md[] = $md_table_row(['---','---','---','---']); $md[] = $md_table_row([ $d['prepared_for'] ?: '—', '—', $d['address'], ($d['title_id'] ?: '—') ]); $md[] = ''; $md[] = '### Relevant Planning Provisions'; $md[] = "- Applicable planning scheme: **{$d['planning_scheme']}**"; if ($d['planning_zones']) $md[] = '- Zone(s): **' . $join($d['planning_zones']) . '**'; if ($d['planning_codes']) $md[] = '- Codes: **' . $join($d['planning_codes']) . '**'; $md[] = ''; // Proposal $md[] = '## 1. Introduction'; $md[] = '**Purpose of the report.** This report seeks planning approval for the proposed use and development described below and assesses the application against the relevant provisions of the Tasmanian Planning Scheme.'; $md[] = ''; $md[] = '## 2. Proposal'; if ($d['proposal_summary']) $md[] = $d['proposal_summary']; $ops = []; if (!empty($d['operations']['hours'])) $ops[] = "**Hours:** {$d['operations']['hours']}"; if (!empty($d['operations']['staff'])) $ops[] = "**Staff:** {$d['operations']['staff']}"; if (!empty($d['operations']['children'])) $ops[] = "**Children capacity:** {$d['operations']['children']}"; if ($ops) $md[] = implode(' \n', $ops); if ($d['parking']) { $pbits = []; if (isset($d['parking']['cars'])) $pbits[] = "{$d['parking']['cars']} car spaces"; if (isset($d['parking']['accessible'])) $pbits[] = "{$d['parking']['accessible']} accessible"; if (isset($d['parking']['bikes'])) $pbits[] = "{$d['parking']['bikes']} bicycle spaces"; if (isset($d['parking']['motorcycle'])) $pbits[] = "{$d['parking']['motorcycle']} motorcycle spaces"; if ($pbits) $md[] = '**Parking:** ' . implode(', ', $pbits) . '.'; } if ($d['signage']) { $md[] = '**Signage:**'; foreach ($d['signage'] as $s) { $md[] = "- " . trim(is_array($s) ? (($s['type'] ?? 'Sign') . ': ' . ($s['desc'] ?? '')) : (string)$s); } } if (!empty($d['map_png'])) { $md[] = ''; $md[] = '![Site and surrounds map]('.$d['map_png'].')'; $md[] = '_Figure: Site context (auto-captured from map)._'; } $md[] = ''; // Site $md[] = '## 3. Site description'; $md[] = $md_table_row(['Property ID','Title ID','Locality','Area','Tenure','LIST GUID']); $md[] = $md_table_row(['---','---','---','---','---','---']); $md[] = $md_table_row([ $d['pid'] ?: '—', $d['title_id'] ?: '—', $d['locality'] ?: '—', $fmt_area($d) ?: '—', $d['tenure'] ?: '—', $d['list_guid'] ?: '—' ]); $md[] = ''; // Zoning $md[] = '## 4. Zoning assessment'; $md[] = "**Zone:** " . ($d['planning_zones'] ? $join($d['planning_zones']) : '—'); $md[] = "**Use class:** {$d['use_class']} (assessment against applicable use and development standards)."; $md[] = ''; // Standards matrix $md[] = '### 4.x Applicable standards (zone + codes overview)'; $md[] = $render_standards($d['standards']); // Codes — notes (driven by the standards rows) $md[] = '## 5. Code assessment'; if (in_array('Signs', $d['planning_codes'])) $md[] = '- **Signs Code** – see relevant rows above.'; if (in_array('Parking and Sustainable Transport', $d['planning_codes'])) $md[] = '- **Parking and Sustainable Transport Code** – see relevant rows above.'; if (in_array('Road and Railway Assets', $d['planning_codes'])) $md[] = '- **Road and Railway Assets Code** – see relevant rows above.'; if (!empty($d['overlays']['bushfire'])) $md[] = '- **Bushfire-Prone Areas Code** – see relevant rows above.'; if (!empty($d['overlays']['airport_noise'])) $md[] = '- **Safeguarding of Airports Code** – see relevant rows above.'; $md[] = ''; // Consultants if ($d['consultants']) { $md[] = '### Supporting technical reports'; foreach ($d['consultants'] as $k => $v) { $md[] = "- **{$k}:** {$v}"; } $md[] = ''; } // Conclusion (quick tally) $t_as = 0; $t_pc = 0; foreach ($d['standards'] as $r) { if (!empty($r['acceptable'])) $t_as++; if (!empty($r['relies_on_pc'])) $t_pc++; } $md[] = '## 6. Conclusion'; $md[] = "This application has been assessed against the Tasmanian Planning Scheme provisions relevant to the site and proposal. Based on the information provided and the supporting technical assessments, the proposal **complies with acceptable solutions for ~{$t_as} standards** and **relies on performance criteria for ~{$t_pc} standards**. Approval is therefore requested subject to any reasonable conditions."; $md[] = ''; // Appendices (labels only) if ($d['appendices']) { $md[] = '## Appendices'; foreach ($d['appendices'] as $i => $label) { $md[] = ($i+1) . ". " . trim((string)$label); } $md[] = ''; } $markdown = implode("\n", $md); // Minimal Markdown → HTML $html = md_to_html_min($markdown); // Optional: save a .md copy (ensure directory exists & is writable) $SAVE_DIR = __DIR__ . '/../../reports'; if (is_dir($SAVE_DIR) && is_writable($SAVE_DIR)) { $fname = sanitize_filename("Planning Report - {$d['address']} - " . date('Ymd_His')) . ".md"; @file_put_contents($SAVE_DIR . '/' . $fname, $markdown); } echo json_encode([ 'ok' => true, 'markdown' => $markdown, 'html' => $html, 'meta' => [ 'address' => $d['address'], 'scheme' => $d['planning_scheme'], 'zones' => $d['planning_zones'], 'codes' => $d['planning_codes'], ], ], JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES); exit; } catch (Throwable $e) { http_response_code(400); echo json_encode(['ok' => false, 'error' => $e->getMessage()]); exit; } /** -------- Helpers -------- */ function sanitize_filename(string $s): string { $s = preg_replace('~[^\w\-. ]+~u', '', $s); $s = preg_replace('~\s+~', ' ', $s); return trim($s) ?: 'report'; } function esc_html(string $s): string { return htmlspecialchars($s, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); } function md_to_html_min(string $md): string { // Extremely small converter: headers, bold/italics, lists, images, tables, paragraphs. $h = esc_html($md); // Images ![alt](src) $h = preg_replace('~!\[([^\]]*)\]\(([^)]+)\)~', '$1', $h); // Bold / italic $h = preg_replace('~\*\*([^*]+)\*\*~', '$1', $h); $h = preg_replace('~\*([^*]+)\*~', '$1', $h); // Headers foreach ([6,5,4,3,2,1] as $n) { $re = '~^' . str_repeat('#', $n) . '\s*(.+)$~m'; $h = preg_replace($re, "$1", $h); } // Tables (GitHub-style) if (preg_match('~^\|.+\|$~m', $h)) { $lines = explode("\n", $h); $out = []; $inTable = false; foreach ($lines as $line) { if (preg_match('~^\|(.+)\|$~', $line)) { if (!$inTable) { $out[] = ''; $inTable = true; } if (preg_match('~^\|\s*-+(\s*\|\s*-+)+\s*\|$~', $line)) continue; // separator $cells = array_map('trim', explode('|', trim($line, '|'))); $out[] = '' . implode('', array_map(fn($c)=>'', $cells)) . ''; } else { if ($inTable) { $out[] = '
'.esc_html($c ?: '–').'
'; $inTable = false; } $out[] = $line; } } if ($inTable) $out[] = ''; $h = implode("\n", $out); } // Lists $h = preg_replace_callback('~(?:^|\n)([-*]\s.+(?:\n[-*]\s.+)*)~m', function($m){ $items = preg_split('~\n~', trim($m[1])); $lis = ''; foreach ($items as $li) { $txt = preg_replace('~^[-*]\s~', '', $li); $lis .= '
  • '. $txt .'
  • '; } return "\n"; }, $h); // Line breaks " \n" →
    $h = str_replace(" \n", "
    \n", $h); // Paragraphs $parts = preg_split('~\n{2,}~', $h); foreach ($parts as &$p) { if (!preg_match('~^\s*<(h\d|ul|ol|table|img|blockquote)~', $p)) { $p = '

    '.$p.'

    '; } } $h = implode("\n", $parts); return '
    '.$h.'
    '; }