| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304 |
- <?php
- // list_lookup.php (with robust cache dir + PID cache)
- // --------------------------------------------------------------
- ini_set('display_errors','0'); error_reporting(E_ALL);
- header('Content-Type: application/json; charset=utf-8');
- // turn notices/warnings into exceptions so we always JSON-error
- set_error_handler(function($severity, $message, $file, $line) {
- throw new ErrorException($message, 0, $severity, $file, $line);
- });
- define('LL_THROTTLE_US', 300000); // 150ms per ArcGIS request
- define('LL_CACHE_TTL', 60*60*24*14); // 14 days
- ob_start();
- try {
- // ---- Input ----
- $raw = file_get_contents('php://input');
- $js = json_decode($raw, true);
- if (!$js || !isset($js['lat'], $js['lng'])) {
- http_response_code(400);
- echo json_encode(['ok'=>false,'error'=>'Missing lat/lng']); exit;
- }
- $lat = (float)$js['lat'];
- $lng = (float)$js['lng'];
- $debug = !empty($js['debug']);
- $full = !empty($js['full']) || !empty($js['debug']);
- // ---- Cache directory selection (writable) ----
- function ll_cache_base() {
- static $base = null;
- if ($base !== null) return $base;
- // 1) env override
- $env = getenv('CACHE_DIR');
- if ($env && @is_dir($env) && @is_writable($env)) {
- $base = rtrim($env, '/');
- return $base;
- }
- // 2) local ./cache (create)
- $local = __DIR__ . '/cache';
- if (!is_dir($local)) { @mkdir($local, 0775, true); }
- if (@is_dir($local) && @is_writable($local)) {
- $base = $local;
- return $base;
- }
- // 3) fallback: /tmp/planning-cache
- $tmp = rtrim(sys_get_temp_dir(), '/').'/planning-cache';
- if (!is_dir($tmp)) { @mkdir($tmp, 0777, true); }
- $base = $tmp;
- return $base;
- }
- function ll_cache_path($key) {
- $safe = preg_replace('/[^a-z0-9_.-]/i','_', $key).'.json';
- return ll_cache_base().'/'.$safe;
- }
- function ll_cache_get($key){
- $f = ll_cache_path($key);
- if (!is_file($f)) return null;
- if (filemtime($f) < time()-LL_CACHE_TTL) return null;
- $s = @file_get_contents($f);
- if ($s === false) return null;
- $j = json_decode($s, true);
- return $j ?: null;
- }
- function ll_cache_set($key,$data){
- $f = ll_cache_path($key);
- if (!is_dir(dirname($f))) @mkdir(dirname($f), 0775, true);
- // Write to a temp file then rename atomically so concurrent requests
- // that miss the cache simultaneously don't both write a partial file.
- $tmp = $f . '.tmp.' . getmypid();
- if (@file_put_contents($tmp, json_encode($data, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE)) !== false) {
- @rename($tmp, $f);
- }
- }
- // Pre-cache key by snapped lat/lng (so we can serve before we know PID)
- $snap = sprintf('%0.6f,%0.6f', $lat, $lng);
- $preKey = "v2-latlng-".$snap;
- if ($cached = ll_cache_get($preKey)) {
- if (!$full) unset($cached['raw']);
- echo json_encode($cached, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
- exit;
- }
- // ---------------- Config ----------------
- $service = 'https://services.thelist.tas.gov.au/arcgis/rest/services/Public/PlanningOnline/MapServer';
- // ---------------- Helpers ----------------
- function arcgis_query($layerId, $lng, $lat, $outFields='*', $returnGeometry=false, $extraParams=[]) {
- global $service;
- $params = array_merge([
- 'f' => 'json',
- 'where' => '1=1',
- 'returnGeometry' => $returnGeometry ? 'true' : 'false',
- 'outFields' => $outFields,
- 'outSR' => '4326',
- 'geometryType' => 'esriGeometryPoint',
- 'spatialRel' => 'esriSpatialRelIntersects',
- 'inSR' => '4326',
- 'geometry' => json_encode(['x'=>(float)$lng, 'y'=>(float)$lat]),
- 'resultRecordCount'=> 200
- ], $extraParams);
- $url = "{$service}/{$layerId}/query";
- $ch = curl_init($url);
- curl_setopt_array($ch, [
- CURLOPT_POST => true,
- CURLOPT_POSTFIELDS => http_build_query($params),
- CURLOPT_RETURNTRANSFER => true,
- CURLOPT_TIMEOUT => 12,
- ]);
- $resp = curl_exec($ch);
- if ($resp === false) {
- $err = curl_error($ch); curl_close($ch);
- return ['error'=>"cURL error: $err"];
- }
- curl_close($ch);
- if (LL_THROTTLE_US > 0) usleep(LL_THROTTLE_US);
- return json_decode($resp, true);
- }
- // Web Mercator area helpers (compute sqm from rings if needed)
- function wmProject($lon, $lat) {
- if ($lat > 85.05112878) $lat = 85.05112878;
- if ($lat < -85.05112878) $lat = -85.05112878;
- $originShift = 20037508.342789244;
- $x = $lon * $originShift / 180.0;
- $y = log(tan((90 + $lat) * M_PI / 360.0)) * $originShift / M_PI;
- return [$x, $y];
- }
- function ringAreaSignedSqm(array $ring) {
- $n = count($ring); if ($n < 3) return 0.0;
- $sum = 0.0;
- for ($i=0; $i<$n; $i++) {
- $j = ($i+1) % $n;
- [$xi,$yi] = wmProject($ring[$i][0], $ring[$i][1]);
- [$xj,$yj] = wmProject($ring[$j][0], $ring[$j][1]);
- $sum += ($xi * $yj) - ($xj * $yi);
- }
- return 0.5 * $sum; // signed
- }
- function polygonAreaFromRingsSqm(array $rings) {
- $total = 0.0;
- foreach ($rings as $ring) $total += ringAreaSignedSqm($ring);
- return abs($total);
- }
- // ---------------- Layer IDs ----------------
- $LAYER_PARCELS = 2; // Cadastral Parcels
- $LAYER_LGA = 8; // Local Government Areas
- $LAYER_ZONES = 13; // Tas Planning Scheme Zones
- $LAYER_CODES = 14; // Tas Planning Scheme Code Overlays
- // ---------------- Parcels (attributes) ----------------
- $parcelsResp = arcgis_query($LAYER_PARCELS, $lng, $lat, '*', false);
- $parcelFeat = ($parcelsResp['features'][0] ?? null);
- $parcelAttr = $parcelFeat['attributes'] ?? [];
- // PID
- $pid = null;
- foreach (['PID','PROPERTY_ID'] as $k) {
- if (!empty($parcelAttr[$k])) { $pid = preg_replace('/[^0-9]/','', (string)$parcelAttr[$k]); break; }
- }
- // ---------------- Parcel geometry (polygon) ----------------
- $parcelGeomResp = arcgis_query($LAYER_PARCELS, $lng, $lat, 'PID,OBJECTID', true);
- $boundaryGeoJSON = null;
- if (!empty($parcelGeomResp['features'][0]['geometry']['rings'])) {
- $rings = $parcelGeomResp['features'][0]['geometry']['rings']; // 4326 due to outSR
- $boundaryGeoJSON = [
- 'type' => 'Feature',
- 'geometry' => [ 'type'=>'Polygon', 'coordinates'=>$rings ],
- 'properties' => [ 'pid'=>$pid ]
- ];
- }
- // ---------------- Title & Area ----------------
- $volume = $parcelAttr['VOLUME'] ?? null;
- $folio = isset($parcelAttr['FOLIO']) ? (string)$parcelAttr['FOLIO'] : null;
- $titleId = ($volume && $folio !== null && $folio !== '') ? ($volume . '/' . $folio)
- : ($parcelAttr['CT'] ?? $parcelAttr['CT_REFERENCE'] ?? null);
- $sqm = null;
- foreach (['MEAS_AREA','COMP_AREA','AREA_SQM','Shape__Area','SHAPE_Area'] as $k) {
- if (isset($parcelAttr[$k]) && is_numeric($parcelAttr[$k])) { $sqm = (float)$parcelAttr[$k]; break; }
- }
- if ($sqm === null && !empty($parcelGeomResp['features'][0]['geometry']['rings'])) {
- $sqm = polygonAreaFromRingsSqm($parcelGeomResp['features'][0]['geometry']['rings']);
- }
- $total_area = null; $area_sqm = null; $area_ha = null;
- if ($sqm !== null) {
- $area_sqm = (float)$sqm;
- $area_ha = (float)$sqm / 10000.0;
- $total_area = [
- 'sqm' => $area_sqm,
- 'sqm_label' => number_format($area_sqm, 0) . ' sqm',
- 'ha' => $area_ha,
- 'ha_label' => rtrim(rtrim(number_format($area_ha, 4, '.', ''), '0'), '.') . ' ha'
- ];
- }
- $tenure = $parcelAttr['TENURE_TY'] ?? null;
- $lpi = $parcelAttr['LPI'] ?? null;
- $listGuid = $parcelAttr['LIST_GUID'] ?? null;
- // ---------------- LGA ----------------
- $lgasResp = arcgis_query($LAYER_LGA, $lng, $lat, '*', false);
- $lgaAttr = ($lgasResp['features'][0]['attributes'] ?? []);
- $council = $lgaAttr['LGA_NAME'] ?? $lgaAttr['NAME'] ?? $lgaAttr['COUNCIL'] ?? null;
- // ---------------- Zones ----------------
- $zonesResp = arcgis_query($LAYER_ZONES, $lng, $lat, '*', false);
- $zoneNames = [];
- $schemeName = null;
- foreach (($zonesResp['features'] ?? []) as $zf) {
- $a = $zf['attributes'] ?? [];
- foreach (['ZONE','ZONING','ZONE_NAME','ZONE_LABEL'] as $k) {
- if (!empty($a[$k])) { $zoneNames[] = $a[$k]; break; }
- }
- if (!$schemeName) $schemeName = $a['LPS'] ?? $a['SCHEME'] ?? 'Tasmanian Planning Scheme';
- }
- $zoneNames = array_values(array_unique(array_filter($zoneNames)));
- // ---------------- Codes ----------------
- $codesResp = arcgis_query($LAYER_CODES, $lng, $lat, '*', false);
- $codeNames = [];
- foreach (($codesResp['features'] ?? []) as $cf) {
- $a = $cf['attributes'] ?? [];
- foreach (['CODE','OVERLAY','OVERLAY_CODE','CODE_NAME','OVERLAY_DESC'] as $k) {
- if (!empty($a[$k])) { $codeNames[] = $a[$k]; break; }
- }
- }
- $codeNames = array_values(array_unique(array_filter($codeNames)));
- // Build compact raw attributes (no geometry except parcel boundary is added separately)
- $raw = [
- 'parcels' => array_values(array_map(
- fn($f) => $f['attributes'] ?? [],
- (array)($parcelsResp['features'] ?? [])
- )),
- 'lga' => array_values(array_map(
- fn($f) => $f['attributes'] ?? [],
- (array)($lgasResp['features'] ?? [])
- )),
- 'zones' => array_values(array_map(
- fn($f) => $f['attributes'] ?? [],
- (array)($zonesResp['features'] ?? [])
- )),
- 'codes' => array_values(array_map(
- fn($f) => $f['attributes'] ?? [],
- (array)($codesResp['features'] ?? [])
- )),
- ];
- // ---------------- Output ----------------
- $out = [
- 'ok' => true,
- 'pid' => $pid,
- 'title_id' => $titleId,
- 'tenure' => $tenure,
- 'lpi' => $lpi,
- 'list_guid' => $listGuid,
- 'total_area' => $total_area,
- 'area_sqm' => $area_sqm,
- 'area_ha' => $area_ha,
- 'council' => $council,
- 'planning_scheme' => $schemeName,
- 'planning_zones' => $zoneNames,
- 'planning_codes' => $codeNames,
- 'boundary' => $boundaryGeoJSON
- ];
- if ($debug) {
- $out['debug'] = [
- 'parcel_attrs' => $parcelAttr,
- 'lga_attrs' => $lgaAttr,
- 'cache_dir' => ll_cache_base(), // show where we’re writing
- ];
- }
- // Cache a “full” version that includes raw attributes
- $cacheOut = $out;
- $cacheOut['raw'] = $raw;
- // Cache under snapped lat/lng immediately…
- ll_cache_set($preKey, $out);
- // …and if we have a PID, also cache under PID (best key)
- if ($pid) ll_cache_set('v2-pid-'.$pid, $cacheOut);
- // Return small by default; include raw only if requested
- $responseOut = $out;
- if ($full) $responseOut['raw'] = $raw;
- echo json_encode($responseOut, JSON_UNESCAPED_SLASHES|JSON_UNESCAPED_UNICODE);
- } catch (Throwable $e) {
- http_response_code(500);
- echo json_encode(['ok'=>false,'error'=>$e->getMessage()]);
- }
|