mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-03-24 16:51:47 -04:00
feat: store zone coordinates as percentages for resolution independence
Convert zone coordinates from absolute pixel values to percentages (0.00-100.00) so zones automatically adapt when monitor resolution changes. This eliminates the need to manually reconfigure zones after resolution adjustments. Changes: - Add DB migration (zm_update-1.37.81.sql) to convert existing pixel coords to percentages, recalculate area, and update Units default - Add Zone::ParsePercentagePolygon() in C++ to parse percentage coords and convert to pixels at runtime using monitor dimensions - Backwards compat: C++ Zone::Load() checks Units column and uses old pixel parser for legacy 'Pixels' zones - Update PHP coordsToPoints/mapCoords/getPolyArea for float coords, replace scanline area algorithm with shoelace formula - Update JS zone editor to work in percentage coordinate space with SVG viewBox "0 0 100 100" and non-scaling-stroke for consistent line thickness - Position zone SVG overlay inside imageFeed container via JS to align with image only (not status bar) - Support array of zone IDs in Monitor::getStreamHTML zones option - Update monitor resize handler: percentage coords don't need rescaling, only threshold pixel counts are adjusted - Add 8 Catch2 unit tests for ParsePercentagePolygon Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained 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',
|
||||
|
||||
110
db/zm_update-1.37.81.sql
Normal file
110
db/zm_update-1.37.81.sql
Normal file
@@ -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';
|
||||
@@ -23,6 +23,8 @@
|
||||
#include "zm_fifo_debug.h"
|
||||
#include "zm_monitor.h"
|
||||
|
||||
#include <cstdlib>
|
||||
|
||||
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<double>(width);
|
||||
double mon_h = static_cast<double>(height);
|
||||
std::vector<Vector2> 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<int32>(std::lround(pct_x * mon_w / 100.0));
|
||||
int32 px_y = static_cast<int32>(std::lround(pct_y * mon_h / 100.0));
|
||||
|
||||
// Clamp to monitor bounds
|
||||
px_x = std::clamp(px_x, static_cast<int32>(0), static_cast<int32>(width));
|
||||
px_y = std::clamp(px_y, static_cast<int32>(0), static_cast<int32>(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> Zone::Load(const std::shared_ptr<Monitor> &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<int32>(monitor->Width())
|
||||
||
|
||||
polygon.Extent().Lo().y_ < 0
|
||||
||
|
||||
polygon.Extent().Hi().y_ > static_cast<int32>(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<int32>(monitor->Width()), static_cast<int32>(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) {
|
||||
|
||||
@@ -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<Zone> Load(const std::shared_ptr<Monitor> &monitor);
|
||||
//=================================================
|
||||
|
||||
@@ -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})
|
||||
|
||||
|
||||
120
tests/zm_zone.cpp
Normal file
120
tests/zm_zone.cpp
Normal file
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#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));
|
||||
}
|
||||
@@ -1208,10 +1208,24 @@ class Monitor extends ZM_Object {
|
||||
}
|
||||
|
||||
if (isset($options['zones']) and $options['zones']) {
|
||||
$html .= '<svg class="zones" id="zones'.$this->Id().'" viewBox="0 0 '.$this->ViewWidth().' '.$this->ViewHeight() .'" preserveAspectRatio="none">'.PHP_EOL;
|
||||
foreach (Zone::find(array('MonitorId'=>$this->Id()), array('order'=>'Area DESC')) as $zone) {
|
||||
$html .= $zone->svg_polygon();
|
||||
} // end foreach zone
|
||||
$html .= '<svg class="zones" id="zones'.$this->Id().'" viewBox="0 0 100 100" preserveAspectRatio="none">'.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
|
||||
</svg>
|
||||
|
||||
@@ -12,7 +12,7 @@ class Zone extends ZM_Object {
|
||||
'MonitorId' => null,
|
||||
'Name' => '',
|
||||
'Type' => 'Active',
|
||||
'Units' => 'Pixels',
|
||||
'Units' => 'Percent',
|
||||
'NumCoords' => '4',
|
||||
'Coords' => '',
|
||||
'Area' => '0',
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<br>',
|
||||
$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<br>');
|
||||
$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<br>',
|
||||
$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<br>', $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<br>');
|
||||
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<br>', $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;
|
||||
|
||||
@@ -329,9 +329,7 @@ svg.zones {
|
||||
left: 0;
|
||||
background: none;
|
||||
width: 100%;
|
||||
/*
|
||||
height: 100%;
|
||||
*/
|
||||
}
|
||||
#videoobj {
|
||||
width: 100%;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.zones polygon {
|
||||
fill-opacity: 0.25;
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.Active {
|
||||
stroke: #ff0000;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
.zones polygon {
|
||||
fill-opacity: 0.25;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.Active {
|
||||
stroke: #ff0000;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.zones polygon {
|
||||
fill-opacity: 0.25;
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.Active {
|
||||
stroke: #ff0000;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
.zones polygon {
|
||||
fill-opacity: 0.25;
|
||||
stroke-width: 2px;
|
||||
vector-effect: non-scaling-stroke;
|
||||
}
|
||||
.Active {
|
||||
stroke: #ff0000;
|
||||
|
||||
@@ -379,7 +379,7 @@ if ($video_tag) {
|
||||
<div class="progressBox" id="progressBox" title="" style="width: 0%;"></div>
|
||||
<div id="indicator" style="display: none;"></div>
|
||||
</div><!--progressBar-->
|
||||
<svg class="zones" id="zones<?php echo $monitor->Id() ?>" style="display:<?php echo $showZones ? 'block' : 'none'; ?>" viewBox="0 0 <?php echo $monitor->ViewWidth().' '.$monitor->ViewHeight() ?>" preserveAspectRatio="none">
|
||||
<svg class="zones" id="zones<?php echo $monitor->Id() ?>" style="display:<?php echo $showZones ? 'block' : 'none'; ?>" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<?php
|
||||
foreach (ZM\Zone::find(array('MonitorId'=>$monitor->Id()), array('order'=>'Area DESC')) as $zone) {
|
||||
echo $zone->svg_polygon();
|
||||
|
||||
@@ -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('<div>');
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -57,9 +57,10 @@ zone['Points'][<?php echo $i ?>] = { 'x': <?php echo $zone['Points'][$i]['x'] ?>
|
||||
}
|
||||
?>
|
||||
|
||||
var maxX = <?php echo $monitor->ViewWidth()-1 ?>;
|
||||
var maxY = <?php echo $monitor->ViewHeight()-1 ?>;
|
||||
var monitorArea = <?php echo $monitor->ViewWidth() * $monitor->ViewHeight() ?>;
|
||||
var maxX = 100;
|
||||
var maxY = 100;
|
||||
var monitorArea = 10000;
|
||||
var monitorPixelArea = <?php echo $monitor->ViewWidth() * $monitor->ViewHeight() ?>;
|
||||
|
||||
var monitorData = new Array();
|
||||
monitorData[monitorData.length] = {
|
||||
|
||||
@@ -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();
|
||||
<?php echo
|
||||
$monitor->getStreamHTML(array('mode'=>'single', 'zones'=>false, 'state'=>true));
|
||||
?>
|
||||
<svg id="zoneSVG" class="zones" viewBox="0 0 <?php echo $monitor->ViewWidth().' '.$monitor->ViewHeight() ?>">
|
||||
<svg id="zoneSVG" class="zones" viewBox="0 0 100 100" preserveAspectRatio="none">
|
||||
<?php
|
||||
if ( $zone['Id'] ) {
|
||||
$other_zones = dbFetchAll('SELECT * FROM Zones WHERE MonitorId = ? AND Id != ?', NULL, array($monitor->Id(), $zone['Id']));
|
||||
|
||||
@@ -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();
|
||||
<tr>
|
||||
<td class="colName"><?php echo makeLink('?view=zone&mid='.$mid.'&zid='.$zone['Id'], validHtmlStr($zone['Name']), true, 'data-on-click-true="streamCmdQuit"'); ?></td>
|
||||
<td class="colType"><?php echo validHtmlStr($zone['Type']) ?></td>
|
||||
<td class="colUnits"><?php echo $zone['Area'] ?> / <?php echo sprintf('%.2f', ($zone['Area']*100)/($monitor->ViewWidth()*$monitor->ViewHeight()) ) ?></td>
|
||||
<td class="colUnits"><?php echo intval($zone['Area'] / 10000 * $monitor->ViewWidth() * $monitor->ViewHeight()) ?> / <?php echo sprintf('%.2f', $zone['Area'] / 100) ?></td>
|
||||
<td class="colMark"><input type="checkbox" name="markZids[]" value="<?php echo $zone['Id'] ?>" data-on-click-this="configureDeleteButton"<?php if ( !canEdit('Monitors') ) { ?> disabled="disabled"<?php } ?>/></td>
|
||||
</tr>
|
||||
<?php
|
||||
|
||||
Reference in New Issue
Block a user