如何在不使用循环的情况下在javascript选取框选择框中查找所选元素?

时间:2013-03-20 19:36:20

标签: javascript

我正在编写自己的拖放文件管理器。这包括一个javascript选框选择框,当活动时,它会计算相交的元素(文件),并通过向它们添加一个类来选择它们。

我目前在mousemove处理程序中执行检查,循环遍历元素坐标数组,并确定哪些与拖放选择框相交。

该功能目前如下所示:

selectItems : function(voidindex){

                        var self = this;
                        var coords = self.cache.selectioncoords;

    for(var i=0, len = self.cache.items.length; i<len; i++){
           var item = self.cache.items[i];
           var itemcoords = item.box_pos;

           if(coords.topleft.x < (itemcoords.x+201) && coords.topright.x > itemcoords.x && coords.topleft.y < (itemcoords.y+221) && coords.bottomleft.y > itemcoords.y){
               if(!item.selected){
                  item.selected = true;
                  item.html.addClass('selected').removeClass('activebutton');
                  self.cache.selecteditems.push(i);
                  self.setInfo();
               }
           }
           else{
               if(item.selected){
                  item.selected = false;
                  if(!voidindex || voidindex !== i){
                      item.html.removeClass('selected');
                  }
                  var removeindex = self.cache.selecteditems.indexOf(i);
                  self.cache.selecteditems.splice(removeindex, 1);
                  self.setInfo();
           }
       }
  }
},

上面的代码中有很多脏逻辑,确保只在选择更改时才操作DOM。这与问题无关,可以被排除在外。重要的部分是交叉逻辑,它检查元素的坐标与选取框选择框的坐标。

另请注意,项目尺寸固定为 201px宽度,221px高度

我已经对此进行了测试,并且一切都运行良好,但是我需要支持可能数千个文件,这意味着在某些时候我们将开始看到UI性能下降。

我想知道是否有反正检测而不循环遍历每个元素的坐标。

在任何给定时间,选框框的坐标定义如下:

 selectioncoords : {
                    topleft : {
                        x : 0,
                        y : 0
                    },
                    topright : {
                        x : 0,
                        y : 0
                    },
                    bottomleft : {
                        x : 0,
                        y : 0
                    },
                    bottomright : {
                        x : 0,
                        y : 0
                    },
                    width : 0,
                    height : 0
                }

存储在self.cache.items数组中的每个项的坐标定义如下:

item : {
       box_pos : {
             x : 0,
             y : 0
       },
       grid_pos : {
              row : 1,
              column : 1
       }


    }

因此,可用信息将始终是实际网格位置(行/列)以及物理项目位置(网格中的左侧和顶部偏移量,以像素为单位)。

总而言之,问题是,无论如何都要从上面定义的一组选取框选择框坐标中检测项目交集,而不是每次mousemove事件触发时都遍历整个项目坐标数组?

提前感谢您的帮助。

4 个答案:

答案 0 :(得分:3)

以下内容取决于具有所述尺寸的锁定网格。

您正在将鼠标定义的矩形与具有静态边缘大小的网格进行比较。因此,给定一个x坐标或一个y坐标,你应该能够很容易地得出坐标所属的列或行(<分别)。

当用户启动选择框时,抓住x和y,找到开头的行/列。当鼠标在拉动选择框的同时移动时,您会找到(然后更新)完成的行/列。选择在该框所定义的行内以及由该框(包括)定义的列内的任何内容。如果您根据行和列将可选元素保存在二维数组中,那么您应该能够以这种方式获取所需的元素。

请注意,这有多大(或更低)效率取决于预期选择框的大小与总大小的比较,以及您期望网格填充的程度。当然,如果平均用例一次选择对象的一半左右,那么就没有很多工作可以有效地减少每次必须查看的对象数量。

此外,尽管它很麻烦,但每次都可以使用mousemove处理程序。让它在更新之间暂停一点会降低这个特定功能的响应能力,但它会大大减少使用的资源量。

答案 1 :(得分:2)

您可以通过为网格中的每个项目建立索引来限制检查范围,这通常是必需不再是。您可以使用网格为您提供 near X,Y坐标或可能处于X1,Y2,X1,Y2范围内的元素列表。

让你入门......

var Grid = function(pixelWidth, pixelHeight, boxSize) {

  this.cellsIn = function(x1, y1, x2, y2) {
    var rv = [];
    for (var x = x1; x < x2; x += boxSize) {
      for (var y = y1; y < y2; y += boxSize) {
        var gx = Math.ceil(x/boxSize);
        var gy = Math.ceil(y/boxSize);
        rv.push(this.cells[gx][gy]);
      }
    }
    return rv;
  } // cellsIn()


  this.add = function(x1, y1, x2, y2, o) {
    var cells = this.cellsIn(x1, y1, x2, y2);
    for (var i in cells) {
      cells[i].push(o);
    }
  } // add()


  this.get = function(x1, y1, x2, y2) {
    var rv = [];
    var rv_index = {};
    var cells = this.cellsIn(x1, y1, x2, y2);
    for (var i in cells) {
      var cell = cells[i];
      for (var oi in cell) {
        if (!rv_index[cell[oi]]) {
          rv_index[cell[oi]] = 1;
          rv.push(cell[oi]);
        }
      }
    }
    return rv;
  } // get()


  this.cells = [];
  for (var x = 0; x < Math.ceil(pixelWidth/boxSize); x++) {
    this.cells[x] = [];
    for (var y = 0; y < Math.ceil(pixelHeight/boxSize); y++) {
      this.cells[x][y] = [];
    }
  }

};

因此,不是迭代所有可能的对象,它们可能是什么,而是迭代在给定坐标中靠近或潜在的所有对象。

这要求您在项目坐标更改时维护/重新索引网格。并且您可能希望为上述(或类似)Grid类添加一些功能来修改/移动现有对象。但是,据我所知,这种索引是最好的(如果不是唯一的)在空间中索引对象的方法。

免责声明:上述代码未经过测试。但是,我有类似的代码。请在此处查看DemoGrid函数类:http://www.thepointless.com/js/ascii_monsters.js

我的DemoGrid的功能类似(据我记得,它已经有一段时间了),但接受x, y, radius作为参数。另外值得注意的是,每次事件触发时,我的鼠标事件都不会触及网格。支票由游戏/主循环限制。

答案 2 :(得分:2)

有几种方法可以解决这个问题。这是一个。首先,您需要某种有组织结构的项目,您可以按行和列快速查找。你可以使用二维数组,或者为了简单起见我将使用哈希表。您可以在创建self.cache.items或更高版本的同时执行此操作:

var cacheLookup = {};

function initCacheLookup() {
    var items = self.cache.items;
    for( var i = 0, n = items.length;  i < n;  i++ ) {
        var item = items[i];
        var key = [ item.grid_pos.row, item.grid_pos.column ].join(',');
        cacheLookup[key] = item;
    }
}

然后,当您想要获得与矩形相交的项目时,您可以执行以下操作:

var itemWidth = 201, itemHeight = 221;

var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth ) + 1;
var right = Math.floor( br.x / itemWidth ) + 1;
var top = Math.floor( tl.y / itemHeight ) + 1;
var bottom = Math.floor( br.y / itemHeight ) + 1;

var selecteditems = [];
for( var row = top;  row <= bottom;  row++ ) {
    for( var col = left;  col <= right;  col++ ) {
        var key = [ row, col ].join(',');
        var item = cacheLookup[key];
        if( item ) {
            selecteditems.push( item );
        }
    }
}
// Now selecteditems has the items intersecting the rectangle

这里可能有一两个错误,但这应该很接近。

好吧,正如我所说,这是一种方法。并且它具有可能有趣的属性,它不依赖于self.cache.items数组中的项的顺序。但是cacheLookup哈希表有点像它可能不是最有效的解决方案。

让我猜一下:这个数组是不是按行和列的顺序正确(反之亦然)?例如,如果您的网格是四宽,那么顶行将是数组元素0-3,第二行4-7,第三行8-11等。或者它可以是沿列向下的类似排列。

假设它是逐行的顺序,那么你根本就不需要哈希表。 initCacheLookup()函数消失了,相反,搜索代码如下所示:

var nCols = 4/*whatever*/;  // defined somewhere else
var itemWidth = 201, itemHeight = 221;

var tl = selectioncoords.topleft, br = selectioncoords.bottomright;
var left = Math.floor( tl.x / itemWidth );
var right = Math.floor( br.x / itemWidth );
var top = Math.floor( tl.y / itemHeight ) * nCols;
var bottom = Math.floor( br.y / itemHeight ) * nCols;

var items = self.cache.items;
var selecteditems = [];
for( var iRow = top;  iRow <= bottom;  iRow += nCols ) {
    for( var col = left;  col <= right;  col++ ) {
        var index = iRow + col;
        if( index < items.length ) {
            selecteditems.push( items[index] );
        }
    }
}
// Now selecteditems has the items intersecting the rectangle

这段代码会更快一些,而且更简单。此外,它完全不依赖于item.box_positem.grid_pos。您可能根本不需要这些数据字段,因为它们很容易根据项目索引,网格列数以及项目高度和宽度进行计算。

一些相关说明:

请勿在代码中对201221进行硬编码。仅将这些变量存储在变量中,然后在需要项目高度和宽度时使用这些变量。

您的数据结构中存在大量重复。除非有特殊需要,否则我建议您无情地删除所有重复数据。具体做法是:

selectioncoords: {
    topleft: {
        x: 0,
        y: 0
    },
    topright: {
        x: 0,
        y: 0
    },
    bottomleft: {
        x: 0,
        y: 0
    },
    bottomright: {
        x: 0,
        y: 0
    },
    width: 0,
    height: 0
}

此处超过一半的数据是重复的或可以计算的。这就是你所需要的:

selectioncoords: {
    left: 0,
    right: 0,
    top: 0,
    bottom: 0
}

我提出这个问题的原因是在处理代码时有点混乱:“我想要左边缘。我是从topleft.x还是bottomleft.x得到的?它们是否真的相同他们看起来像什么?我该怎么选?“

此外,如上所述,如果项目存储在顺序数组中,则可能根本不需要item.box_positem.grid_pos。如果需要它们,你可以只存储一个并从中计算另一个,因为两者之间存在直接关系:

box_pos.x === ( grid_pos.column - 1 ) * itemWidth
box_pos.y === ( grid_pos.row - 1 ) * itemHeight

答案 3 :(得分:0)

如果系统设置为

  • self.cache.items从左到右,从上到下排序
    • (0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1,2) ,(2,2)
  • 每个空间都有一个项目
    • 好 - (0,0),(1,0),(2,0),(0,1),(1,1),(1,2),(0,2),(1, 2),(2,2)
    • BAD - (0,0),(2,0)(1,2),(1,3),(2,1),(2,3)
  • 我们需要知道列的总数。

所以让你开始的代码。

// Some 'constants' we'll need.
number_of_columns = 4;
item_width = 201;
item_height = 221;

// First off, we are dealing with a grid system, 
// so that means that if given the starting x and y of the marquee,
// we can determine which element in the cache to start where we begin.
top_left_selected_index = Math.floor(selectioncoords.topleft.x / item_width) + (Math.floor(selectioncoords.topright.y / item_height) * number_of_columns );

// Now, because the array is in order, and there are no empty cache points, 
// we know that the lower bound of the selected items is `top_left_selected_index`
// so all we have to do is walk the array to grab the other selected.

number_columns_selected = (selectioncoords.bottomright.x - selectioncoords.topleft.x) / item_width;
// if it it doesn't divide exactly it means there is an extra column selected
if((selectioncoords.bottomright.x - selectioncoords.topleft.x) % item_width > 0){
  number_columns_selected += 1;
}

// if it it doesn't divide exactly it means there is an extra column selected
number_rows_selected = (selectioncoords.bottomright.y - selectioncoords.topleft.y) / item_height;
if((selectioncoords.bottomright.y - selectioncoords.topleft.y) % item_height > 0){
  number_rows_selected += 1;
}

// Outer loop handles the moving the pointer in terms of the row, so it
// increments by the number of columns.
// EX: Given my simple example array, To get from (1,0) to (1,1) 
// requires an index increase of 3
for(i=0; i < number_rows_selected; i++){
  // Inner loop marches through the the columns, so it is just one at a time.
  // Added j < number_of_columns in case your marquee stretches well past your content
  for(j=0; j < number_columns_selected && j < number_of_columns; j++){
    // Do stuff to the selected items.
    self.cache.items[top_left_selected_index + (i * number_of_columns) + j];
  }
}