diff --git a/db/zm_create.sql.in b/db/zm_create.sql.in index cd0a5a4d0..df6f3b610 100644 --- a/db/zm_create.sql.in +++ b/db/zm_create.sql.in @@ -883,7 +883,7 @@ CREATE TABLE `ZonePresets` ( `Id` int(10) unsigned NOT NULL auto_increment, `Name` varchar(64) NOT NULL default '', `Type` enum('Active','Inclusive','Exclusive','Preclusive','Inactive','Privacy') NOT NULL default 'Active', - `Units` enum('Pixels','Percent') NOT NULL default 'Pixels', + `Units` enum('Pixels','Percent') NOT NULL default 'Percent', `CheckMethod` enum('AlarmedPixels','FilteredPixels','Blobs') NOT NULL default 'Blobs', `MinPixelThreshold` smallint(5) unsigned default NULL, `MaxPixelThreshold` smallint(5) unsigned default NULL, @@ -913,7 +913,7 @@ CREATE TABLE `Zones` ( FOREIGN KEY (`MonitorId`) REFERENCES `Monitors` (`Id`) ON DELETE CASCADE, `Name` varchar(64) NOT NULL default '', `Type` enum('Active','Inclusive','Exclusive','Preclusive','Inactive','Privacy') NOT NULL default 'Active', - `Units` enum('Pixels','Percent') NOT NULL default 'Pixels', + `Units` enum('Pixels','Percent') NOT NULL default 'Percent', `NumCoords` tinyint(3) unsigned NOT NULL default '0', `Coords` tinytext NOT NULL, `Area` int(10) unsigned NOT NULL default '0', diff --git a/db/zm_update-1.37.81.sql b/db/zm_update-1.37.81.sql new file mode 100644 index 000000000..5c4ac498a --- /dev/null +++ b/db/zm_update-1.37.81.sql @@ -0,0 +1,110 @@ +-- +-- This updates a 1.37.80 database to 1.37.81 +-- +-- Convert Zone Coords from pixel values to percentage values (0.00-100.00) +-- so that zones are resolution-independent. +-- + +DELIMITER // + +DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent` // + +CREATE PROCEDURE `zm_update_zone_coords_to_percent`() +BEGIN + DECLARE done INT DEFAULT FALSE; + DECLARE v_zone_id INT; + DECLARE v_coords TINYTEXT; + DECLARE v_mon_width INT; + DECLARE v_mon_height INT; + DECLARE v_new_coords TEXT DEFAULT ''; + DECLARE v_pair TEXT; + DECLARE v_x_str TEXT; + DECLARE v_y_str TEXT; + DECLARE v_x_pct TEXT; + DECLARE v_y_pct TEXT; + DECLARE v_remaining TEXT; + DECLARE v_space_pos INT; + + DECLARE cur CURSOR FOR + SELECT z.Id, z.Coords, m.Width, m.Height + FROM Zones z + JOIN Monitors m ON z.MonitorId = m.Id + WHERE m.Width > 0 AND m.Height > 0; + + DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = TRUE; + + OPEN cur; + + read_loop: LOOP + FETCH cur INTO v_zone_id, v_coords, v_mon_width, v_mon_height; + IF done THEN + LEAVE read_loop; + END IF; + + -- Skip if coords already look like percentages (contain a decimal point) + IF v_coords LIKE '%.%' THEN + ITERATE read_loop; + END IF; + + SET v_new_coords = ''; + SET v_remaining = TRIM(v_coords); + + -- Parse each space-separated x,y pair + coord_loop: LOOP + IF v_remaining = '' OR v_remaining IS NULL THEN + LEAVE coord_loop; + END IF; + + SET v_space_pos = LOCATE(' ', v_remaining); + IF v_space_pos > 0 THEN + SET v_pair = LEFT(v_remaining, v_space_pos - 1); + SET v_remaining = TRIM(SUBSTRING(v_remaining, v_space_pos + 1)); + ELSE + SET v_pair = v_remaining; + SET v_remaining = ''; + END IF; + + -- Skip empty pairs (from double spaces, trailing commas etc) + IF v_pair = '' OR v_pair = ',' THEN + ITERATE coord_loop; + END IF; + + -- Split on comma + SET v_x_str = SUBSTRING_INDEX(v_pair, ',', 1); + SET v_y_str = SUBSTRING_INDEX(v_pair, ',', -1); + + -- Convert to percentage with 2 decimal places + -- Use CAST to DECIMAL which always uses '.' as decimal separator (locale-independent) + SET v_x_pct = CAST(ROUND(CAST(v_x_str AS DECIMAL(10,2)) / v_mon_width * 100, 2) AS DECIMAL(10,2)); + SET v_y_pct = CAST(ROUND(CAST(v_y_str AS DECIMAL(10,2)) / v_mon_height * 100, 2) AS DECIMAL(10,2)); + + IF v_new_coords != '' THEN + SET v_new_coords = CONCAT(v_new_coords, ' '); + END IF; + SET v_new_coords = CONCAT(v_new_coords, v_x_pct, ',', v_y_pct); + + END LOOP coord_loop; + + IF v_new_coords != '' THEN + UPDATE Zones SET Coords = v_new_coords WHERE Id = v_zone_id; + END IF; + + END LOOP read_loop; + + CLOSE cur; +END // + +DELIMITER ; + +CALL zm_update_zone_coords_to_percent(); +DROP PROCEDURE IF EXISTS `zm_update_zone_coords_to_percent`; + +-- Recalculate Area from pixel-space to percentage-space (100x100 = 10000 for full frame) +UPDATE Zones z + JOIN Monitors m ON z.MonitorId = m.Id + SET z.Area = ROUND(z.Area * 10000.0 / (m.Width * m.Height)) + WHERE m.Width > 0 AND m.Height > 0 AND z.Area > 0; + +-- Update Units to Percent for all zones, and set as new default +UPDATE Zones SET Units = 'Percent' WHERE Units = 'Pixels'; +ALTER TABLE Zones ALTER Units SET DEFAULT 'Percent'; diff --git a/src/zm_zone.cpp b/src/zm_zone.cpp index 3aec13122..034d6e263 100644 --- a/src/zm_zone.cpp +++ b/src/zm_zone.cpp @@ -23,6 +23,8 @@ #include "zm_fifo_debug.h" #include "zm_monitor.h" +#include + void Zone::Setup( ZoneType p_type, const Polygon &p_polygon, @@ -792,6 +794,47 @@ bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon) { return !vertices.empty(); } // end bool Zone::ParsePolygonString(const char *poly_string, Polygon &polygon) +bool Zone::ParsePercentagePolygon(const char *poly_string, unsigned int width, unsigned int height, Polygon &polygon) { + double mon_w = static_cast(width); + double mon_h = static_cast(height); + std::vector vertices; + const char *str = poly_string; + + while (*str != '\0') { + const char *cp = strchr(str, ','); + if (!cp) { + Error("Bogus coordinate %s found in polygon string", str); + break; + } + + double pct_x = strtod(str, nullptr); + double pct_y = strtod(cp + 1, nullptr); + int32 px_x = static_cast(std::lround(pct_x * mon_w / 100.0)); + int32 px_y = static_cast(std::lround(pct_y * mon_h / 100.0)); + + // Clamp to monitor bounds + px_x = std::clamp(px_x, static_cast(0), static_cast(width)); + px_y = std::clamp(px_y, static_cast(0), static_cast(height)); + + Debug(3, "Percentage coord %.2f,%.2f -> pixel %d,%d", pct_x, pct_y, px_x, px_y); + vertices.emplace_back(px_x, px_y); + + const char *ws = strchr(cp + 2, ' '); + if (ws) { + str = ws + 1; + } else { + break; + } + } + + if (vertices.size() > 2) { + polygon = Polygon(vertices); + return true; + } + Error("Not enough coordinates to form a polygon from '%s'", poly_string); + return false; +} // end bool Zone::ParsePercentagePolygon + bool Zone::ParseZoneString(const char *zone_string, unsigned int &zone_id, int &colour, Polygon &polygon) { Debug(3, "Parsing zone string '%s'", zone_string); @@ -891,48 +934,22 @@ std::vector Zone::Load(const std::shared_ptr &monitor) { /* HTML colour code is actually BGR in memory, we want RGB */ AlarmRGB = rgb_convert(AlarmRGB, ZM_SUBPIX_ORDER_BGR); - Debug(5, "Parsing polygon %s", Coords); + Debug(5, "Parsing polygon %s (Units=%s)", Coords, Units); Polygon polygon; - if ( !ParsePolygonString(Coords, polygon) ) { - Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring", Coords, Id, Name, monitor->Name()); - continue; - } - - if (polygon.Extent().Lo().x_ < 0 - || - polygon.Extent().Hi().x_ > static_cast(monitor->Width()) - || - polygon.Extent().Lo().y_ < 0 - || - polygon.Extent().Hi().y_ > static_cast(monitor->Height())) { - Error("Zone %d/%s for monitor %s extends outside of image dimensions, (%d,%d), (%d,%d) != (%d,%d), fixing", - Id, - Name, - monitor->Name(), - polygon.Extent().Lo().x_, - polygon.Extent().Lo().y_, - polygon.Extent().Hi().x_, - polygon.Extent().Hi().y_, - monitor->Width(), - monitor->Height()); - - auto n_coords = polygon.GetVertices().size(); - polygon.Clip(Box( - {0, 0}, - {static_cast(monitor->Width()), static_cast(monitor->Height())} - )); - if (polygon.GetVertices().size() != n_coords) { - Error("Cropping altered the number of vertices! From %zu to %zu", n_coords, polygon.GetVertices().size()); + if (!strcmp(Units, "Pixels")) { + // Legacy pixel-based coordinates: parse as integer pixel values + if (!ParsePolygonString(Coords, polygon)) { + Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring", + Coords, Id, Name, monitor->Name()); + continue; + } + } else { + // Percentage-based coordinates (default): convert to pixels using monitor dimensions + if (!ParsePercentagePolygon(Coords, monitor->Width(), monitor->Height(), polygon)) { + Error("Unable to parse polygon string '%s' for zone %d/%s for monitor %s, ignoring", + Coords, Id, Name, monitor->Name()); + continue; } - } - - if ( false && !strcmp( Units, "Percent" ) ) { - MinAlarmPixels = (MinAlarmPixels*polygon.Area())/100; - MaxAlarmPixels = (MaxAlarmPixels*polygon.Area())/100; - MinFilterPixels = (MinFilterPixels*polygon.Area())/100; - MaxFilterPixels = (MaxFilterPixels*polygon.Area())/100; - MinBlobPixels = (MinBlobPixels*polygon.Area())/100; - MaxBlobPixels = (MaxBlobPixels*polygon.Area())/100; } if (atoi(dbrow[2]) == Zone::INACTIVE) { diff --git a/src/zm_zone.h b/src/zm_zone.h index 917319a41..f626dacbe 100644 --- a/src/zm_zone.h +++ b/src/zm_zone.h @@ -211,6 +211,7 @@ class Zone { std::string DumpSettings(bool verbose) const; static bool ParsePolygonString( const char *polygon_string, Polygon &polygon ); + static bool ParsePercentagePolygon(const char *poly_string, unsigned int width, unsigned int height, Polygon &polygon); static bool ParseZoneString( const char *zone_string, unsigned int &zone_id, int &colour, Polygon &polygon ); static std::vector Load(const std::shared_ptr &monitor); //================================================= diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 72d599d76..508a6ac38 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -19,7 +19,8 @@ set(TEST_SOURCES zm_onvif_renewal.cpp zm_poly.cpp zm_utils.cpp - zm_vector2.cpp) + zm_vector2.cpp + zm_zone.cpp) add_executable(tests main.cpp ${TEST_SOURCES}) diff --git a/tests/zm_zone.cpp b/tests/zm_zone.cpp new file mode 100644 index 000000000..8e5dbbb46 --- /dev/null +++ b/tests/zm_zone.cpp @@ -0,0 +1,120 @@ +/* + * This file is part of the ZoneMinder Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the + * Free Software Foundation; either version 2 of the License, or (at your + * option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see . + */ + +#include "zm_catch2.h" + +#include "zm_zone.h" + +TEST_CASE("Zone::ParsePercentagePolygon: full-frame zone at 1920x1080", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00", + 1920, 1080, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices().size() == 4); + REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0)); + REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080)); + REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080)); +} + +TEST_CASE("Zone::ParsePercentagePolygon: center 50% zone", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "25.00,25.00 75.00,25.00 75.00,75.00 25.00,75.00", + 1920, 1080, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices().size() == 4); + REQUIRE(polygon.GetVertices()[0] == Vector2(480, 270)); + REQUIRE(polygon.GetVertices()[1] == Vector2(1440, 270)); + REQUIRE(polygon.GetVertices()[2] == Vector2(1440, 810)); + REQUIRE(polygon.GetVertices()[3] == Vector2(480, 810)); +} + +TEST_CASE("Zone::ParsePercentagePolygon: fractional percentages", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "50.25,75.50 60.00,75.50 60.00,85.00 50.25,85.00", + 1920, 1080, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices().size() == 4); + // 50.25% of 1920 = 964.8 -> 965 + REQUIRE(polygon.GetVertices()[0].x_ == 965); + // 75.50% of 1080 = 815.4 -> 815 + REQUIRE(polygon.GetVertices()[0].y_ == 815); +} + +TEST_CASE("Zone::ParsePercentagePolygon: different resolution", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00", + 640, 480, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices()[1] == Vector2(640, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(640, 480)); +} + +TEST_CASE("Zone::ParsePercentagePolygon: clamping beyond 100%", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "0.00,0.00 110.00,0.00 100.00,100.00 0.00,100.00", + 1920, 1080, polygon); + + REQUIRE(result == true); + // 110% should be clamped to monitor width + REQUIRE(polygon.GetVertices()[1].x_ == 1920); +} + +TEST_CASE("Zone::ParsePercentagePolygon: triangle", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "50.00,10.00 90.00,90.00 10.00,90.00", + 1000, 1000, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices().size() == 3); + REQUIRE(polygon.GetVertices()[0] == Vector2(500, 100)); + REQUIRE(polygon.GetVertices()[1] == Vector2(900, 900)); + REQUIRE(polygon.GetVertices()[2] == Vector2(100, 900)); +} + +TEST_CASE("Zone::ParsePercentagePolygon: too few points", "[Zone]") { + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "0.00,0.00 100.00,0.00", + 1920, 1080, polygon); + + REQUIRE(result == false); +} + +TEST_CASE("Zone::ParsePercentagePolygon: integer coords still work", "[Zone]") { + // strtod handles integers fine + Polygon polygon; + bool result = Zone::ParsePercentagePolygon( + "0,0 100,0 100,100 0,100", + 1920, 1080, polygon); + + REQUIRE(result == true); + REQUIRE(polygon.GetVertices()[0] == Vector2(0, 0)); + REQUIRE(polygon.GetVertices()[1] == Vector2(1920, 0)); + REQUIRE(polygon.GetVertices()[2] == Vector2(1920, 1080)); + REQUIRE(polygon.GetVertices()[3] == Vector2(0, 1080)); +} diff --git a/web/includes/Monitor.php b/web/includes/Monitor.php index 69a38203d..ed2f4890c 100644 --- a/web/includes/Monitor.php +++ b/web/includes/Monitor.php @@ -1208,10 +1208,24 @@ class Monitor extends ZM_Object { } if (isset($options['zones']) and $options['zones']) { - $html .= ''.PHP_EOL; - foreach (Zone::find(array('MonitorId'=>$this->Id()), array('order'=>'Area DESC')) as $zone) { - $html .= $zone->svg_polygon(); - } // end foreach zone + $html .= ''.PHP_EOL; + if (is_array($options['zones'])) { + // Render specific zone IDs only + foreach ($options['zones'] as $zone_id) { + $zone = new Zone($zone_id); + if ($zone->Id() and $zone->MonitorId() == $this->Id()) { + $html .= $zone->svg_polygon(); + } + } + } else { + // true: render all zones for this monitor + foreach (Zone::find(array('MonitorId'=>$this->Id()), array('order'=>'Area DESC')) as $zone) { + $html .= $zone->svg_polygon(); + } + } + if (isset($options['zones_extra'])) { + $html .= $options['zones_extra']; + } $html .= ' Sorry, your browser does not support inline SVG diff --git a/web/includes/Zone.php b/web/includes/Zone.php index 89ac41ba7..c7a67cecd 100644 --- a/web/includes/Zone.php +++ b/web/includes/Zone.php @@ -12,7 +12,7 @@ class Zone extends ZM_Object { 'MonitorId' => null, 'Name' => '', 'Type' => 'Active', - 'Units' => 'Pixels', + 'Units' => 'Percent', 'NumCoords' => '4', 'Coords' => '', 'Area' => '0', diff --git a/web/includes/actions/monitor.php b/web/includes/actions/monitor.php index 3c3a1619e..386531c5c 100644 --- a/web/includes/actions/monitor.php +++ b/web/includes/actions/monitor.php @@ -181,27 +181,22 @@ if ($action == 'save') { $zones = dbFetchAll('SELECT * FROM Zones WHERE MonitorId=?', NULL, array($mid)); + // Zone coords are stored as percentages, so they don't need rescaling + // on resolution change. Only handle rotation (swap x/y) and rescale + // threshold pixel counts. + $newA = $newW * $newH; + $oldA = $oldMonitor->Width() * $oldMonitor->Height(); + if ( ($newW == $oldMonitor->Height()) and ($newH == $oldMonitor->Width()) ) { + // Rotation: swap x,y percentage coords foreach ( $zones as $zone ) { - $newZone = $zone; - # Rotation, no change to area etc just swap the coords $newZone = $zone; $points = coordsToPoints($zone['Coords']); for ( $i = 0; $i < count($points); $i++ ) { $x = $points[$i]['x']; $points[$i]['x'] = $points[$i]['y']; $points[$i]['y'] = $x; - - if ( $points[$i]['x'] > ($newW-1) ) { - ZM\Warning("Correcting x {$points[$i]['x']} > $newW of zone {$newZone['Name']} as it extends outside the new dimensions"); - $points[$i]['x'] = ($newW-1); - } - if ( $points[$i]['y'] > ($newH-1) ) { - ZM\Warning("Correcting y {$points[$i]['y']} $newH of zone {$newZone['Name']} as it extends outside the new dimensions"); - $points[$i]['y'] = ($newH-1); - } } - $newZone['Coords'] = pointsToCoords($points); $changes = getFormChanges($zone, $newZone, $types); @@ -210,33 +205,17 @@ if ($action == 'save') { array($mid, $zone['Id'])); } } # end foreach zone - } else { - $newA = $newW * $newH; - $oldA = $oldMonitor->Width() * $oldMonitor->Height(); - + } else if ($oldA > 0 && $newA != $oldA) { + // Non-rotation resize: coords stay the same (percentages), + // but rescale threshold pixel counts by area ratio foreach ( $zones as $zone ) { $newZone = $zone; - $points = coordsToPoints($zone['Coords']); - for ( $i = 0; $i < count($points); $i++ ) { - $points[$i]['x'] = intval(($points[$i]['x']*($newW-1))/($oldMonitor->Width()-1)); - $points[$i]['y'] = intval(($points[$i]['y']*($newH-1))/($oldMonitor->Height()-1)); - if ( $points[$i]['x'] > ($newW-1) ) { - ZM\Warning("Correcting x of zone {$newZone['Name']} as it extends outside the new dimensions"); - $points[$i]['x'] = ($newW-1); - } - if ( $points[$i]['y'] > ($newH-1) ) { - ZM\Warning("Correcting y of zone {$newZone['Name']} as it extends outside the new dimensions"); - $points[$i]['y'] = ($newH-1); - } - } - $newZone['Coords'] = pointsToCoords($points); - $newZone['Area'] = intval(round(($zone['Area']*$newA)/$oldA)); - $newZone['MinAlarmPixels'] = intval(round(($newZone['MinAlarmPixels']*$newA)/$oldA)); - $newZone['MaxAlarmPixels'] = intval(round(($newZone['MaxAlarmPixels']*$newA)/$oldA)); - $newZone['MinFilterPixels'] = intval(round(($newZone['MinFilterPixels']*$newA)/$oldA)); - $newZone['MaxFilterPixels'] = intval(round(($newZone['MaxFilterPixels']*$newA)/$oldA)); - $newZone['MinBlobPixels'] = intval(round(($newZone['MinBlobPixels']*$newA)/$oldA)); - $newZone['MaxBlobPixels'] = intval(round(($newZone['MaxBlobPixels']*$newA)/$oldA)); + $newZone['MinAlarmPixels'] = intval(round(($zone['MinAlarmPixels']*$newA)/$oldA)); + $newZone['MaxAlarmPixels'] = intval(round(($zone['MaxAlarmPixels']*$newA)/$oldA)); + $newZone['MinFilterPixels'] = intval(round(($zone['MinFilterPixels']*$newA)/$oldA)); + $newZone['MaxFilterPixels'] = intval(round(($zone['MaxFilterPixels']*$newA)/$oldA)); + $newZone['MinBlobPixels'] = intval(round(($zone['MinBlobPixels']*$newA)/$oldA)); + $newZone['MaxBlobPixels'] = intval(round(($zone['MaxBlobPixels']*$newA)/$oldA)); $changes = getFormChanges($zone, $newZone, $types); @@ -261,26 +240,20 @@ if ($action == 'save') { if ( $monitor->insert($changes) ) { $mid = $monitor->Id(); - // Adjust zone dimensions if monitor has rotation applied + // Zone coords are now stored as percentages (0-100) $zoneWidth = $newMonitor['Width']; $zoneHeight = $newMonitor['Height']; if (isset($newMonitor['Orientation']) && ($newMonitor['Orientation'] == 'ROTATE_90' || $newMonitor['Orientation'] == 'ROTATE_270')) { - // Swap dimensions for 90/270 degree rotations $zoneWidth = $newMonitor['Height']; $zoneHeight = $newMonitor['Width']; } $zoneArea = $zoneWidth * $zoneHeight; $zone = new ZM\Zone(); - if (!$zone->save(['MonitorId'=>$monitor->Id(), 'Name'=>'All', 'Coords'=> - sprintf( '%d,%d %d,%d %d,%d %d,%d', 0, 0, - $zoneWidth-1, - 0, - $zoneWidth-1, - $zoneHeight-1, - 0, - $zoneHeight-1), - 'Area'=>$zoneArea, + if (!$zone->save(['MonitorId'=>$monitor->Id(), 'Name'=>'All', + 'Units'=>'Percent', + 'Coords'=>'0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00', + 'Area'=>10000, 'MinAlarmPixels'=>intval(($zoneArea*.05)/100), 'MaxAlarmPixels'=>intval(($zoneArea*75)/100), 'MinFilterPixels'=>intval(($zoneArea*.05)/100), diff --git a/web/includes/actions/zone.php b/web/includes/actions/zone.php index 57469c151..e48da16f1 100644 --- a/web/includes/actions/zone.php +++ b/web/includes/actions/zone.php @@ -32,13 +32,15 @@ if ( !empty($_REQUEST['mid']) && canEdit('Monitors', $_REQUEST['mid']) ) { } if ( $_REQUEST['newZone']['Units'] == 'Percent' ) { + // Convert percentage thresholds to pixel counts using actual monitor pixel area + $pixelArea = $monitor->ViewWidth() * $monitor->ViewHeight(); foreach (array( 'MinAlarmPixels','MaxAlarmPixels', 'MinFilterPixels','MaxFilterPixels', 'MinBlobPixels','MaxBlobPixels' ) as $field ) { if ( isset($_REQUEST['newZone'][$field]) and $_REQUEST['newZone'][$field] ) - $_REQUEST['newZone'][$field] = intval(($_REQUEST['newZone'][$field]*$_REQUEST['newZone']['Area'])/100); + $_REQUEST['newZone'][$field] = intval(($_REQUEST['newZone'][$field]*$pixelArea)/100); } } diff --git a/web/includes/functions.php b/web/includes/functions.php index 591342090..6d9954bea 100644 --- a/web/includes/functions.php +++ b/web/includes/functions.php @@ -1384,96 +1384,16 @@ function _CompareX($a, $b) { } function getPolyArea($points) { - global $debug; - - $n_coords = count($points); - $global_edges = array(); - for ( $j = 0, $i = $n_coords-1; $j < $n_coords; $i = $j++ ) { - $x1 = $points[$i]['x']; - $x2 = $points[$j]['x']; - $y1 = $points[$i]['y']; - $y2 = $points[$j]['y']; - - //printf( "x1:%d,y1:%d x2:%d,y2:%d\n", x1, y1, x2, y2 ); - if ( $y1 == $y2 ) - continue; - - $dx = $x2 - $x1; - $dy = $y2 - $y1; - - $global_edges[] = array( - 'min_y' => $y1<$y2?$y1:$y2, - 'max_y' => ($y1<$y2?$y2:$y1)+1, - 'min_x' => $y1<$y2?$x1:$x2, - '_1_m' => $dx/$dy, - ); - } - - usort($global_edges, '_CompareXY'); - - if ( $debug ) { - for ( $i = 0; $i < count($global_edges); $i++ ) { - printf('%d: min_y: %d, max_y:%d, min_x:%.2f, 1/m:%.2f
', - $i, - $global_edges[$i]['min_y'], - $global_edges[$i]['max_y'], - $global_edges[$i]['min_x'], - $global_edges[$i]['_1_m']); - } - } - + // Shoelace formula - works correctly with both integer and float coordinates + $n = count($points); $area = 0.0; - $active_edges = array(); - $y = $global_edges[0]['min_y']; - do { - for ( $i = 0; $i < count($global_edges); $i++ ) { - if ( $global_edges[$i]['min_y'] == $y ) { - if ( $debug ) printf('Moving global edge
'); - $active_edges[] = $global_edges[$i]; - array_splice($global_edges, $i, 1); - $i--; - } else { - break; - } - } - usort($active_edges, '_CompareX'); - if ( $debug ) { - for ( $i = 0; $i < count($active_edges); $i++ ) { - printf('%d - %d: min_y: %d, max_y:%d, min_x:%.2f, 1/m:%.2f
', - $y, $i, - $active_edges[$i]['min_y'], - $active_edges[$i]['max_y'], - $active_edges[$i]['min_x'], - $active_edges[$i]['_1_m']); - } - } - $last_x = 0; - $row_area = 0; - $parity = false; - for ( $i = 0; $i < count($active_edges); $i++ ) { - $x = intval(round($active_edges[$i]['min_x'])); - if ( $parity ) { - $row_area += ($x - $last_x)+1; - $area += $row_area; - } - if ( $active_edges[$i]['max_y'] != $y ) - $parity = !$parity; - $last_x = $x; - } - if ( $debug ) printf('%d: Area:%d
', $y, $row_area); - $y++; - for ( $i = 0; $i < count($active_edges); $i++ ) { - if ( $y >= $active_edges[$i]['max_y'] ) { // Or >= as per sheets - if ( $debug ) printf('Deleting active_edge
'); - array_splice($active_edges, $i, 1); - $i--; - } else { - $active_edges[$i]['min_x'] += $active_edges[$i]['_1_m']; - } - } - } while ( count($global_edges) || count($active_edges) ); - if ( $debug ) printf('Area:%d
', $area); - return $area; + for ($i = 0; $i < $n - 1; $i++) { + $area += ((float)$points[$i]['x'] * (float)$points[$i+1]['y'] + - (float)$points[$i+1]['x'] * (float)$points[$i]['y']); + } + $area += ((float)$points[$n-1]['x'] * (float)$points[0]['y'] + - (float)$points[0]['x'] * (float)$points[$n-1]['y']); + return round(abs($area) / 2.0); } function getPolyAreaOld($points) { @@ -1502,7 +1422,7 @@ function getPolyAreaOld($points) { } function mapCoords($a) { - return $a['x'].','.$a['y']; + return number_format((float)$a['x'], 2, '.', '').','.number_format((float)$a['y'], 2, '.', ''); } function pointsToCoords($points) { @@ -1511,10 +1431,10 @@ function pointsToCoords($points) { function coordsToPoints($coords) { $points = array(); - if ( preg_match_all('/(\d+,\d+)+/', $coords, $matches) ) { + if ( preg_match_all('/([\d.]+,[\d.]+)+/', $coords, $matches) ) { for ( $i = 0; $i < count($matches[1]); $i++ ) { - if ( preg_match('/(\d+),(\d+)/', $matches[1][$i], $cmatches) ) { - $points[] = array('x'=>$cmatches[1], 'y'=>$cmatches[2]); + if ( preg_match('/([\d.]+),([\d.]+)/', $matches[1][$i], $cmatches) ) { + $points[] = array('x'=>(float)$cmatches[1], 'y'=>(float)$cmatches[2]); } else { echo('Bogus coordinates ('.$matches[$i].')'); return false; diff --git a/web/skins/classic/css/base/views/event.css b/web/skins/classic/css/base/views/event.css index d23913018..0e8761cdf 100644 --- a/web/skins/classic/css/base/views/event.css +++ b/web/skins/classic/css/base/views/event.css @@ -329,9 +329,7 @@ svg.zones { left: 0; background: none; width: 100%; - /* height: 100%; - */ } #videoobj { width: 100%; diff --git a/web/skins/classic/css/base/views/zone.css b/web/skins/classic/css/base/views/zone.css index 87ed25b9b..2adbe3356 100644 --- a/web/skins/classic/css/base/views/zone.css +++ b/web/skins/classic/css/base/views/zone.css @@ -43,12 +43,16 @@ #imageFrame .monitor { width: 100%; } +#imageFrame .imageFeed { + position: relative; +} #imageFrame svg { - box-sizing: border-box; - position:absolute; - top: 3px; - left: 3px; - right: 3px; + box-sizing: border-box; + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; background: none; } #imageFrame img { @@ -117,9 +121,11 @@ .zones polygon { fill-opacity: 0.25; stroke-width: 0; + vector-effect: non-scaling-stroke; } .zones polygon.Editing { stroke-width: 2px; + vector-effect: non-scaling-stroke; } .Active { stroke: #ff0000; diff --git a/web/skins/classic/css/base/views/zones.css b/web/skins/classic/css/base/views/zones.css index 55e78c2f2..d805222b2 100644 --- a/web/skins/classic/css/base/views/zones.css +++ b/web/skins/classic/css/base/views/zones.css @@ -1,6 +1,7 @@ .zones polygon { fill-opacity: 0.25; stroke-width: 2px; + vector-effect: non-scaling-stroke; } .Active { stroke: #ff0000; diff --git a/web/skins/classic/css/base/zones.css b/web/skins/classic/css/base/zones.css index 82c96e6b2..7dcdca468 100644 --- a/web/skins/classic/css/base/zones.css +++ b/web/skins/classic/css/base/zones.css @@ -1,5 +1,6 @@ .zones polygon { fill-opacity: 0.25; + vector-effect: non-scaling-stroke; } .Active { stroke: #ff0000; diff --git a/web/skins/classic/css/classic/views/zones.css b/web/skins/classic/css/classic/views/zones.css index 9952e5db4..1fa4c4b4c 100644 --- a/web/skins/classic/css/classic/views/zones.css +++ b/web/skins/classic/css/classic/views/zones.css @@ -1,6 +1,7 @@ .zones polygon { fill-opacity: 0.25; stroke-width: 2px; + vector-effect: non-scaling-stroke; } .Active { stroke: #ff0000; diff --git a/web/skins/classic/css/dark/views/zones.css b/web/skins/classic/css/dark/views/zones.css index 9952e5db4..1fa4c4b4c 100644 --- a/web/skins/classic/css/dark/views/zones.css +++ b/web/skins/classic/css/dark/views/zones.css @@ -1,6 +1,7 @@ .zones polygon { fill-opacity: 0.25; stroke-width: 2px; + vector-effect: non-scaling-stroke; } .Active { stroke: #ff0000; diff --git a/web/skins/classic/views/event.php b/web/skins/classic/views/event.php index e6f3d8d16..e6c25eae5 100644 --- a/web/skins/classic/views/event.php +++ b/web/skins/classic/views/event.php @@ -379,7 +379,7 @@ if ($video_tag) {
- + $monitor->Id()), array('order'=>'Area DESC')) as $zone) { echo $zone->svg_polygon(); diff --git a/web/skins/classic/views/js/zone.js b/web/skins/classic/views/js/zone.js index b39e91c39..219bd9196 100644 --- a/web/skins/classic/views/js/zone.js +++ b/web/skins/classic/views/js/zone.js @@ -210,25 +210,28 @@ function toPercent(field, maxValue) { } function applyZoneUnits() { - var area = zone.Area; + // zone.Area is in percentage-space (0-10000 for full frame) + // Threshold fields are stored as pixel counts in the DB + // Convert to pixel area for threshold display conversions + var pixelArea = Math.round(zone.Area / monitorArea * monitorPixelArea); var form = document.zoneForm; if ( form.elements['newZone[Units]'].value == 'Pixels' ) { - form.elements['newZone[Area]'].value = area; - toPixels(form.elements['newZone[MinAlarmPixels]'], area); - toPixels(form.elements['newZone[MaxAlarmPixels]'], area); - toPixels(form.elements['newZone[MinFilterPixels]'], area); - toPixels(form.elements['newZone[MaxFilterPixels]'], area); - toPixels(form.elements['newZone[MinBlobPixels]'], area); - toPixels(form.elements['newZone[MaxBlobPixels]'], area); + form.elements['newZone[Area]'].value = pixelArea; + toPixels(form.elements['newZone[MinAlarmPixels]'], pixelArea); + toPixels(form.elements['newZone[MaxAlarmPixels]'], pixelArea); + toPixels(form.elements['newZone[MinFilterPixels]'], pixelArea); + toPixels(form.elements['newZone[MaxFilterPixels]'], pixelArea); + toPixels(form.elements['newZone[MinBlobPixels]'], pixelArea); + toPixels(form.elements['newZone[MaxBlobPixels]'], pixelArea); } else { - form.elements['newZone[Area]'].value = Math.round(area/monitorArea * 100); - toPercent(form.elements['newZone[MinAlarmPixels]'], area); - toPercent(form.elements['newZone[MaxAlarmPixels]'], area); - toPercent(form.elements['newZone[MinFilterPixels]'], area); - toPercent(form.elements['newZone[MaxFilterPixels]'], area); - toPercent(form.elements['newZone[MinBlobPixels]'], area); - toPercent(form.elements['newZone[MaxBlobPixels]'], area); + form.elements['newZone[Area]'].value = Math.round(zone.Area/monitorArea * 100); + toPercent(form.elements['newZone[MinAlarmPixels]'], pixelArea); + toPercent(form.elements['newZone[MaxAlarmPixels]'], pixelArea); + toPercent(form.elements['newZone[MinFilterPixels]'], pixelArea); + toPercent(form.elements['newZone[MaxFilterPixels]'], pixelArea); + toPercent(form.elements['newZone[MinBlobPixels]'], pixelArea); + toPercent(form.elements['newZone[MaxBlobPixels]'], pixelArea); } } @@ -255,9 +258,12 @@ function limitFilter(field) { function limitArea(field) { var minValue = 0; - var maxValue = zone.Area; + var maxValue; if ( document.zoneForm.elements['newZone[Units]'].value == 'Percent' ) { maxValue = 100; + } else { + // In Pixels mode, max is the zone's pixel area + maxValue = Math.round(zone.Area / monitorArea * monitorPixelArea); } if (maxValue > 0) { limitRange(field, minValue, maxValue); @@ -295,7 +301,7 @@ function unsetActivePoint(index) { function getCoordString() { var coords = []; for ( let i = 0; i < zone['Points'].length; i++ ) { - coords[coords.length] = zone['Points'][i].x+','+zone['Points'][i].y; + coords[coords.length] = parseFloat(zone['Points'][i].x).toFixed(2)+','+parseFloat(zone['Points'][i].y).toFixed(2); } return coords.join(' '); } @@ -330,29 +336,28 @@ function constrainValue(value, loVal, hiVal) { function updateActivePoint(index) { const point = $j('#point'+index); - const imageFrame = document.getElementById('imageFrame'); - const style = imageFrame.currentStyle || window.getComputedStyle(imageFrame); - const padding_left = parseInt(style.paddingLeft); - const padding_top = parseInt(style.paddingTop); - const padding_right = parseInt(style.paddingRight); - const scale = (imageFrame.clientWidth - ( padding_left + padding_right )) / maxX; + const imageFeed = document.getElementById('imageFeed'+zone.MonitorId); + const frameW = imageFeed.clientWidth; + const frameH = imageFeed.clientHeight; let point_left = parseInt(point.css('left'), 10); - if ( point_left < padding_left ) { - point.css('left', style.paddingLeft); - point_left = parseInt(padding_left); + if ( point_left < 0 ) { + point.css('left', '0px'); + point_left = 0; } let point_top = parseInt(point.css('top')); - if ( point_top < padding_top ) { - point.css('top', style.paddingTop); - point_top = parseInt(padding_top); + if ( point_top < 0 ) { + point.css('top', '0px'); + point_top = 0; } - var x = constrainValue(Math.ceil(point_left / scale)-Math.ceil(padding_left/scale), 0, maxX); - var y = constrainValue(Math.ceil(point_top / scale)-Math.ceil(padding_top/scale), 0, maxY); + var x = constrainValue(Math.round((point_left / frameW) * maxX * 100) / 100, 0, maxX); + var y = constrainValue(Math.round((point_top / frameH) * maxY * 100) / 100, 0, maxY); - zone['Points'][index].x = document.getElementById('newZone[Points]['+index+'][x]').value = x; - zone['Points'][index].y = document.getElementById('newZone[Points]['+index+'][y]').value = y; + zone['Points'][index].x = x; + zone['Points'][index].y = y; + document.getElementById('newZone[Points]['+index+'][x]').value = x.toFixed(2); + document.getElementById('newZone[Points]['+index+'][y]').value = y.toFixed(2); var Point = document.getElementById('zonePoly').points.getItem(index); Point.x = x; Point.y = y; @@ -365,8 +370,8 @@ function addPoint(index) { nextIndex = 0; } - var newX = parseInt(Math.round((zone['Points'][index]['x']+zone['Points'][nextIndex]['x'])/2)); - var newY = parseInt(Math.round((zone['Points'][index]['y']+zone['Points'][nextIndex]['y'])/2)); + var newX = Math.round(((parseFloat(zone['Points'][index]['x'])+parseFloat(zone['Points'][nextIndex]['x']))/2) * 100) / 100; + var newY = Math.round(((parseFloat(zone['Points'][index]['y'])+parseFloat(zone['Points'][nextIndex]['y']))/2) * 100) / 100; if ( nextIndex == 0 ) { zone['Points'][zone['Points'].length] = {'x': newX, 'y': newY}; } else { @@ -381,19 +386,19 @@ function delPoint(index) { } function limitPointValue(point, loVal, hiVal) { - point.value = constrainValue(point.value, loVal, hiVal); + point.value = constrainValue(parseFloat(point.value), loVal, hiVal); } function updateArea( ) { + // Area is calculated in percentage coordinate space (0-100 x 0-100) const area = Polygon_calcArea(zone['Points']); + zone.Area = area; const form = document.getElementById('zoneForm'); - form.elements['newZone[Area]'].value = area; - if ( form.elements['newZone[Units]'].value == 'Percent' ) { - form.elements['newZone[Area]'].value = Math.round( area/monitorArea*100 ); - } else if ( form.elements['newZone[Units]'].value == 'Pixels' ) { - form.elements['newZone[Area]'].value = area; + // Display in current units mode + if ( form.elements['newZone[Units]'].value == 'Pixels' ) { + form.elements['newZone[Area]'].value = Math.round(area / monitorArea * monitorPixelArea); } else { - alert('Unknown units: ' + form.elements['newZone[Units]'].value); + form.elements['newZone[Area]'].value = Math.round(area / monitorArea * 100); } } @@ -404,14 +409,11 @@ function updateX(input) { limitPointValue(input, 0, maxX); const point = $j('#point'+index); - const x = input.value; - const imageFrame = document.getElementById('imageFrame'); - const style = imageFrame.currentStyle || window.getComputedStyle(imageFrame); - const padding_left = parseInt(style.paddingLeft); - const padding_right = parseInt(style.paddingRight); - const scale = (imageFrame.clientWidth - ( padding_left + padding_right )) / maxX; + const x = parseFloat(input.value); + const imageFeed = document.getElementById('imageFeed'+zone.MonitorId); + const frameW = imageFeed.clientWidth; - point.css('left', parseInt(x*scale)+'px'); + point.css('left', Math.round(x / maxX * frameW) + 'px'); zone['Points'][index].x = x; const Point = document.getElementById('zonePoly').points.getItem(index); Point.x = x; @@ -424,14 +426,11 @@ function updateY(input) { limitPointValue(input, 0, maxY); const point = $j('#point'+index); - const y = input.value; - const imageFrame = document.getElementById('imageFrame'); - const style = imageFrame.currentStyle || window.getComputedStyle(imageFrame); - const padding_left = parseInt(style.paddingLeft); - const padding_right = parseInt(style.paddingRight); - const scale = (imageFrame.clientWidth - ( padding_left + padding_right )) / maxX; + const y = parseFloat(input.value); + const imageFeed = document.getElementById('imageFeed'+zone.MonitorId); + const frameH = imageFeed.clientHeight; - point.css('top', parseInt(y*scale)+'px'); + point.css('top', Math.round(y / maxY * frameH) + 'px'); zone['Points'][index].y = y; const Point = document.getElementById('zonePoly').points.getItem(index); Point.y = y; @@ -454,13 +453,10 @@ function saveChanges(element) { } function drawZonePoints() { - var imageFrame = document.getElementById('imageFrame'); + var imageFeed = document.getElementById('imageFeed'+zone.MonitorId); $j('.zonePoint').remove(); - var style = imageFrame.currentStyle || window.getComputedStyle(imageFrame); - var padding_left = parseInt(style.paddingLeft); - var padding_right = parseInt(style.paddingRight); - var padding_top = parseInt(style.paddingTop); - var scale = (imageFrame.clientWidth - ( padding_left + padding_right )) / maxX; + var frameW = imageFeed.clientWidth; + var frameH = imageFeed.clientHeight; $j.each( zone['Points'], function(i, coord) { var div = $j('
'); @@ -471,17 +467,17 @@ function drawZonePoints() { 'title': 'Point '+(i+1) }); div.css({ - left: (Math.round(coord.x * scale) + padding_left)+"px", - top: ((parseInt(coord.y * scale)) + padding_top) +"px" + left: Math.round(parseFloat(coord.x) / maxX * frameW)+"px", + top: Math.round(parseFloat(coord.y) / maxY * frameH)+"px" }); div.mouseover(highlightOn.bind(i, i)); div.mouseout(highlightOff.bind(i, i)); - $j('#imageFrame').append(div); + $j(imageFeed).append(div); div.draggable({ - 'containment': document.getElementById('imageFeed'+zone.MonitorId), + 'containment': imageFeed, 'start': setActivePoint.bind(i, i), 'stop': fixActivePoint.bind(i, i), 'drag': updateActivePoint.bind(i, i) @@ -505,11 +501,12 @@ function drawZonePoints() { $j(input).attr({ 'id': 'newZone[Points]['+i+'][x]', 'name': 'newZone[Points]['+i+'][x]', - 'value': zone['Points'][i].x, + 'value': parseFloat(zone['Points'][i].x).toFixed(2), 'type': 'number', 'class': 'ZonePoint', 'min': '0', 'max': maxX, + 'step': 'any', 'data-point-index': i }); input.oninput = window['updateX'].bind(input, input); @@ -521,11 +518,12 @@ function drawZonePoints() { $j(input).attr({ 'id': 'newZone[Points]['+i+'][y]', 'name': 'newZone[Points]['+i+'][y]', - 'value': zone['Points'][i].y, + 'value': parseFloat(zone['Points'][i].y).toFixed(2), 'type': 'number', 'class': 'ZonePoint', 'min': '0', 'max': maxY, + 'step': 'any', 'data-point-index': i }); input.oninput = window['updateY'].bind(input, input); @@ -710,6 +708,14 @@ function initPage() { monitors[i].start(); } + // Move the zone SVG into the imageFeed container so it overlays only the image, + // not the status bar below it. + var imageFeed = document.getElementById('imageFeed'+zone.MonitorId); + var zoneSVG = document.getElementById('zoneSVG'); + if (imageFeed && zoneSVG) { + imageFeed.appendChild(zoneSVG); + } + document.querySelectorAll('#imageFrame img').forEach(function(el) { el.addEventListener("load", imageLoadEvent, {passive: true}); }); @@ -755,11 +761,10 @@ function Polygon_calcArea(coords) { var float_area = 0.0; for ( i = 0; i < n_coords-1; i++ ) { - var trap_area = (coords[i].x*coords[i+1].y - coords[i+1].x*coords[i].y) / 2; + var trap_area = (parseFloat(coords[i].x)*parseFloat(coords[i+1].y) - parseFloat(coords[i+1].x)*parseFloat(coords[i].y)) / 2; float_area += trap_area; - //printf( "%.2f (%.2f)\n", float_area, trap_area ); } - float_area += (coords[n_coords-1].x*coords[0].y - coords[0].x*coords[n_coords-1].y) / 2; + float_area += (parseFloat(coords[n_coords-1].x)*parseFloat(coords[0].y) - parseFloat(coords[0].x)*parseFloat(coords[n_coords-1].y)) / 2; return Math.round(Math.abs(float_area)); } diff --git a/web/skins/classic/views/js/zone.js.php b/web/skins/classic/views/js/zone.js.php index aae51a9b6..2cb8ebcc1 100644 --- a/web/skins/classic/views/js/zone.js.php +++ b/web/skins/classic/views/js/zone.js.php @@ -57,9 +57,10 @@ zone['Points'][] = { 'x': } ?> -var maxX = ViewWidth()-1 ?>; -var maxY = ViewHeight()-1 ?>; -var monitorArea = ViewWidth() * $monitor->ViewHeight() ?>; +var maxX = 100; +var maxY = 100; +var monitorArea = 10000; +var monitorPixelArea = ViewWidth() * $monitor->ViewHeight() ?>; var monitorData = new Array(); monitorData[monitorData.length] = { diff --git a/web/skins/classic/views/zone.php b/web/skins/classic/views/zone.php index f1301514a..a965677b3 100644 --- a/web/skins/classic/views/zone.php +++ b/web/skins/classic/views/zone.php @@ -55,9 +55,9 @@ foreach ( getEnumValues('Zones', 'CheckMethod') as $optCheckMethod ) { $monitor = new ZM\Monitor($mid); $minX = 0; -$maxX = $monitor->ViewWidth()-1; +$maxX = 100; $minY = 0; -$maxY = $monitor->ViewHeight()-1; +$maxY = 100; if ( !isset($zone) ) { if ( $zid > 0 ) { @@ -67,11 +67,11 @@ if ( !isset($zone) ) { 'Id' => 0, 'Name' => translate('New'), 'Type' => 'Active', - 'Units' => 'Pixels', + 'Units' => 'Percent', 'MonitorId' => $monitor->Id(), 'NumCoords' => 4, - 'Coords' => sprintf('%d,%d %d,%d, %d,%d %d,%d', $minX, $minY, $maxX, $minY, $maxX, $maxY, $minX, $maxY), - 'Area' => $monitor->ViewWidth() * $monitor->ViewHeight(), + 'Coords' => '0.00,0.00 100.00,0.00 100.00,100.00 0.00,100.00', + 'Area' => 10000, 'AlarmRGB' => 0xff0000, 'CheckMethod' => 'Blobs', 'MinPixelThreshold' => '', @@ -142,7 +142,7 @@ echo getNavBarHTML(); getStreamHTML(array('mode'=>'single', 'zones'=>false, 'state'=>true)); ?> - + Id(), $zone['Id'])); diff --git a/web/skins/classic/views/zones.php b/web/skins/classic/views/zones.php index 5d16cf7be..9a991a2fe 100644 --- a/web/skins/classic/views/zones.php +++ b/web/skins/classic/views/zones.php @@ -57,9 +57,9 @@ echo getNavBarHTML(); $monitor = $monitors[$mid]; # ViewWidth() and ViewHeight() are already rotated $minX = 0; - $maxX = $monitor->ViewWidth()-1; + $maxX = 100; $minY = 0; - $maxY = $monitor->ViewHeight()-1; + $maxY = 100; $zones = array(); foreach ( dbFetchAll('SELECT * FROM Zones WHERE MonitorId=? ORDER BY Area DESC', NULL, array($mid)) as $row ) { @@ -93,7 +93,7 @@ echo getNavBarHTML();
-  / ViewWidth()*$monitor->ViewHeight()) ) ?> + ViewWidth() * $monitor->ViewHeight()) ?> /