"Coastal Waters from Sandy Hook to Manasquan Inlet",
"ANZ451" => "Coastal Waters from Manasquan Inlet to Little Egg Inlet",
"ANZ452" => "Coastal Waters from Little Egg Inlet to Great Egg Inlet",
"ANZ453" => "Coastal Waters from Great Egg Inlet to Cape May",
"ANZ454" => "Coastal Waters from Cape May to Cape Henlopen",
"ANZ430" => "Delaware Bay Waters North of East Point and Slaughter Beach",
"ANZ431" => "Delaware Bay Waters South of East Point and Slaughter Beach",
"ANZ338" => "New York Harbor",
"ANZ355" => "Sandy Hook to Fire Island Inlet"
];
$shorelineCounties = [
"Bergen", "Essex", "Gloucester", "Hudson", "Hunterdon",
"Mercer", "Middlesex", "Monmouth", "Morris", "Ocean",
"Passaic", "Somerset", "Sussex", "Union", "Warren"
];
$allowedShorelineAlerts = [
"Coastal Flood Advisory",
"Coastal Flood Warning",
"Coastal Flood Watch",
"Coastal Flood Statement",
"High Surf Advisory",
"High Surf Warning",
"Rip Current Statement",
"Tropical Storm Watch",
"Tropical Storm Warning",
"Hurricane Watch",
"Hurricane Warning"
];
/* ------------------------------ HTTP helpers ------------------------------ */
function httpGet(string $url, string $accept = "application/json"): array
{
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_FOLLOWLOCATION => true,
CURLOPT_CONNECTTIMEOUT => 12,
CURLOPT_TIMEOUT => 25,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_ENCODING => "",
CURLOPT_USERAGENT => APP_USER_AGENT,
CURLOPT_HTTPHEADER => [
"Accept: {$accept}",
"Cache-Control: no-cache"
]
]);
$body = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
return [
"ok" => !$error && $status >= 200 && $status < 300 && is_string($body),
"status" => $status,
"error" => $error,
"body" => is_string($body) ? $body : ""
];
}
function cachedGet(string $url, string $accept = "application/json"): array
{
$cacheDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . "nj_marine_dashboard";
if (!is_dir($cacheDir)) {
@mkdir($cacheDir, 0775, true);
}
$cacheFile = $cacheDir . DIRECTORY_SEPARATOR . sha1($url . "|" . $accept) . ".cache";
if (is_file($cacheFile) && (time() - (int) filemtime($cacheFile)) < CACHE_SECONDS) {
$cached = @file_get_contents($cacheFile);
if (is_string($cached) && $cached !== "") {
return ["ok" => true, "status" => 200, "error" => "", "body" => $cached];
}
}
$result = httpGet($url, $accept);
if ($result["ok"]) {
@file_put_contents($cacheFile, $result["body"], LOCK_EX);
} elseif (is_file($cacheFile)) {
$stale = @file_get_contents($cacheFile);
if (is_string($stale) && $stale !== "") {
return ["ok" => true, "status" => 200, "error" => "", "body" => $stale];
}
}
return $result;
}
function sendJson(array $payload, int $status = 200): void
{
http_response_code($status);
header("Content-Type: application/json; charset=utf-8");
header("Cache-Control: no-store, no-cache, must-revalidate");
echo json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
exit;
}
function safeJsonDecode(string $body): array
{
$data = json_decode($body, true);
return is_array($data) ? $data : [];
}
function h(?string $value): string
{
return htmlspecialchars((string) $value, ENT_QUOTES, "UTF-8");
}
/* --------------------------- Same-file API proxy --------------------------- */
if (isset($_GET["api"])) {
$api = (string) $_GET["api"];
if ($api === "tides") {
$station = preg_replace('/\D/', '', (string) ($_GET["station"] ?? ""));
$product = (string) ($_GET["product"] ?? "");
$allowedProducts = [
"predictions",
"water_level",
"water_temperature",
"air_temperature",
"air_pressure"
];
if (!$station || !in_array($product, $allowedProducts, true)) {
sendJson(["error" => ["message" => "Invalid tide request."]], 400);
}
$params = [
"product" => $product,
"application" => "NewJerseyMarineDashboard",
"station" => $station,
"time_zone" => "lst_ldt",
"units" => "english",
"format" => "json"
];
if ($product === "predictions") {
$begin = preg_replace('/\D/', '', (string) ($_GET["begin_date"] ?? ""));
$end = preg_replace('/\D/', '', (string) ($_GET["end_date"] ?? ""));
if (strlen($begin) !== 8 || strlen($end) !== 8) {
sendJson(["error" => ["message" => "Invalid tide dates."]], 400);
}
$params["begin_date"] = $begin;
$params["end_date"] = $end;
$params["datum"] = "MLLW";
$params["interval"] = "hilo";
} else {
$params["date"] = "latest";
if ($product === "water_level") {
$params["datum"] = "MLLW";
}
}
$url = "https://api.tidesandcurrents.noaa.gov/api/prod/datagetter?" .
http_build_query($params, "", "&", PHP_QUERY_RFC3986);
$result = cachedGet($url, "application/json");
if (!$result["ok"]) {
sendJson(["error" => ["message" => "NOAA tide service is unavailable."]], 502);
}
header("Content-Type: application/json; charset=utf-8");
header("Cache-Control: no-store");
echo $result["body"];
exit;
}
if ($api === "marine-alerts") {
$zones = $GLOBALS["marineZones"];
$all = [];
$seen = [];
foreach (array_keys($zones) as $zone) {
$url = "https://api.weather.gov/alerts/active?zone=" . rawurlencode($zone);
$result = cachedGet($url, "application/geo+json");
if (!$result["ok"]) {
continue;
}
$data = safeJsonDecode($result["body"]);
foreach (($data["features"] ?? []) as $feature) {
if (!is_array($feature)) continue;
$id = (string) ($feature["id"] ?? sha1(json_encode($feature)));
if (isset($seen[$id])) continue;
$seen[$id] = true;
$properties = $feature["properties"] ?? [];
$affectedZones = [];
foreach (($properties["affectedZones"] ?? []) as $zoneUrl) {
if (preg_match('~/zones/(?:forecast|county|fire)/([A-Z0-9]+)$~', (string) $zoneUrl, $m)) {
if (isset($zones[$m[1]])) $affectedZones[] = $m[1];
}
}
if (!$affectedZones) {
$areaText = (string) ($properties["areaDesc"] ?? "");
foreach (array_keys($zones) as $zoneCode) {
if (stripos($areaText, $zoneCode) !== false) $affectedZones[] = $zoneCode;
}
}
$all[] = [
"id" => $id,
"event" => (string) ($properties["event"] ?? "Marine Alert"),
"headline" => (string) ($properties["headline"] ?? ""),
"description" => (string) ($properties["description"] ?? ""),
"instruction" => (string) ($properties["instruction"] ?? ""),
"areaDesc" => (string) ($properties["areaDesc"] ?? ""),
"effective" => $properties["effective"] ?? null,
"expires" => $properties["expires"] ?? null,
"zones" => array_values(array_unique($affectedZones))
];
}
}
sendJson(["alerts" => $all, "checked" => date(DATE_ATOM)]);
}
if ($api === "shoreline-alerts") {
$url = "https://api.weather.gov/alerts/active?area=NJ";
$result = cachedGet($url, "application/geo+json");
if (!$result["ok"]) {
sendJson(["error" => ["message" => "NWS shoreline alert service is unavailable."]], 502);
}
$data = safeJsonDecode($result["body"]);
$counties = $GLOBALS["shorelineCounties"];
$allowed = $GLOBALS["allowedShorelineAlerts"];
$alerts = [];
foreach (($data["features"] ?? []) as $feature) {
$p = $feature["properties"] ?? [];
$event = (string) ($p["event"] ?? "");
$area = (string) ($p["areaDesc"] ?? "");
if (!in_array($event, $allowed, true)) continue;
$matched = [];
foreach ($counties as $county) {
if (stripos($area, $county) !== false) $matched[] = $county;
}
if (!$matched) continue;
$alerts[] = [
"id" => (string) ($feature["id"] ?? sha1(json_encode($feature))),
"event" => $event,
"headline" => (string) ($p["headline"] ?? ""),
"description" => (string) ($p["description"] ?? ""),
"instruction" => (string) ($p["instruction"] ?? ""),
"areaDesc" => $area,
"counties" => $matched,
"effective" => $p["effective"] ?? null,
"expires" => $p["expires"] ?? null
];
}
sendJson(["alerts" => $alerts, "checked" => date(DATE_ATOM)]);
}
sendJson(["error" => ["message" => "Unknown API request."]], 404);
}
/* ------------------------- Marine forecast parsing ------------------------- */
function cleanForecastText(string $text): string
{
$text = str_replace("\r", "", $text);
$text = preg_replace('/\n{3,}/', "\n\n", $text) ?? $text;
return trim($text);
}
function extractPeriods(string $raw): array
{
if ($raw === "") return [];
$raw = cleanForecastText($raw);
// Individual zone files normally contain a single zone block. Remove headers.
$start = preg_match('/\n\.(?:TODAY|TONIGHT|THIS AFTERNOON|THIS EVENING|OVERNIGHT|REST OF TODAY|REST OF TONIGHT|MON|TUE|WED|THU|FRI|SAT|SUN)[A-Z ]*\.\.\./i', $raw, $m, PREG_OFFSET_CAPTURE)
? (int) $m[0][1]
: 0;
if ($start > 0) {
$raw = substr($raw, $start);
}
$raw = preg_replace('/\n\$\$.*$/s', '', $raw) ?? $raw;
$pattern = '/(?:^|\n)\.([A-Z][A-Z ]+?)\.\.\.(.*?)(?=(?:\n\.[A-Z][A-Z ]+?\.\.\.)|\n\$\$|\z)/s';
preg_match_all($pattern, $raw, $matches, PREG_SET_ORDER);
$periods = [];
foreach ($matches as $match) {
$name = trim($match[1]);
$text = trim(preg_replace('/\s+/', ' ', $match[2]) ?? $match[2]);
if ($name !== "" && $text !== "") {
$periods[] = [
"name" => ucwords(strtolower($name)),
"text" => $text
];
}
}
if (!$periods && trim($raw) !== "") {
$fallback = trim(preg_replace('/\s+/', ' ', $raw) ?? $raw);
if ($fallback !== "") {
$periods[] = ["name" => "Marine Forecast", "text" => $fallback];
}
}
return $periods;
}
$marineForecasts = [];
foreach ($marineZones as $zone => $name) {
$url = "https://tgftp.nws.noaa.gov/data/forecasts/marine/coastal/an/" .
strtolower($zone) . ".txt";
$result = cachedGet($url, "text/plain");
$periods = $result["ok"] ? extractPeriods($result["body"]) : [];
$marineForecasts[$zone] = [
"name" => $name,
"periods" => $periods
];
}
?>
New Jersey Marine Dashboard
Marine Alerts
Select a zone to filter active watches, warnings and advisories.
Checking…
▼
ALL ZONES
$name): ?>
= h($zone) ?>
Loading active marine alerts…
🌊 NJ Shoreline Alerts
Coastal flood, high surf, rip current and tropical alerts for selected New Jersey counties.
Checking…
▼
Loading shoreline alerts…
Loading NOAA tide information…
Today’s Tides
Time Tide Height
Loading tides…
Tomorrow’s Tides
Time Tide Height
Loading tides…
Refresh Data
Auto-refresh every 5 minutes
Tide predictions and observations are supplied by NOAA CO-OPS. Predictions use feet above mean lower low water. Some subordinate stations provide predictions but no live observations.
New Jersey Marine Forecasts
Select a marine zone to view the latest National Weather Service forecast.
Server checked = h(date("g:i A T")) ?>
▼
$name): ?>
" type="button" data-forecast-zone="= h($zone) ?>">= h($zone) ?>
$data): ?>
">
= h($zone) ?> — = h($data["name"]) ?>
Forecast data is temporarily unavailable for this marine zone.
= h($period["name"]) ?>
= h($period["text"]) ?>
Forecast source: NOAA/National Weather Service