| Line 162... |
Line 162... |
| 162 |
double firstCircleX = circles[0];
|
162 |
double firstCircleX = circles[0];
|
| 163 |
double secondCircleX = circles[1];
|
163 |
double secondCircleX = circles[1];
|
| 164 |
double distance = Math.abs(firstCircleX - secondCircleX);
|
164 |
double distance = Math.abs(firstCircleX - secondCircleX);
|
| 165 |
LOGGER.info("Distance is " + distance);
|
165 |
LOGGER.info("Distance is " + distance);
|
| 166 |
|
166 |
|
| 167 |
// Closed loop: no assumed ratio, every move re-measures.
|
167 |
// Closed loop on the ring's own position; no assumed ratio.
|
| 168 |
int moveByOffset = aim(driver, actionProvider, distance);
|
168 |
int moveByOffset = aim(driver, actionProvider, circles);
|
| 169 |
if (moveByOffset > 0) {
|
169 |
if (moveByOffset > 0) {
|
| 170 |
actionProvider.release().perform();
|
170 |
actionProvider.release().perform();
|
| 171 |
pressed = false;
|
171 |
pressed = false;
|
| 172 |
LOGGER.info("Move click performed (main {}px)", moveByOffset);
|
172 |
LOGGER.info("Move click performed (main {}px)", moveByOffset);
|
| 173 |
try {
|
173 |
try {
|
| Line 422... |
Line 422... |
| 422 |
* measurable movement, and it must be too small to cross the hole. */
|
422 |
* measurable movement, and it must be too small to cross the hole. */
|
| 423 |
private static final int SEED_NUDGE = 12;
|
423 |
private static final int SEED_NUDGE = 12;
|
| 424 |
private static final int MAX_AIM_STEPS = 5;
|
424 |
private static final int MAX_AIM_STEPS = 5;
|
| 425 |
private static final int MAX_STEP = 140;
|
425 |
private static final int MAX_STEP = 140;
|
| 426 |
|
426 |
|
| - |
|
427 |
/** Centre of the draggable ring, in page CSS px, or null if it is not there. */
|
| - |
|
428 |
private Double pieceCentreX(WebDriver driver) {
|
| - |
|
429 |
try {
|
| - |
|
430 |
WebElement piece = driver.findElement(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_sub-slider_')]"));
|
| - |
|
431 |
return piece.getLocation().getX() + (piece.getSize().getWidth() / 2.0);
|
| - |
|
432 |
} catch (Exception e) {
|
| - |
|
433 |
return null;
|
| - |
|
434 |
}
|
| - |
|
435 |
}
|
| - |
|
436 |
|
| 427 |
/**
|
437 |
/**
|
| 428 |
* Drag onto the hole without ever assuming a ratio.
|
438 |
* Drag the ring onto the hole, deriving px-per-px as we go.
|
| - |
|
439 |
*
|
| - |
|
440 |
* Why this does not re-run circle detection: it cannot. Hough needs to resolve TWO
|
| - |
|
441 |
* circles of similar radius, and as the ring closes on the hole they overlap and stop
|
| - |
|
442 |
* being two -- so the measurement dies exactly at convergence. Measured in production
|
| - |
|
443 |
* after r37437: 25 of 36 attempts lost the circles on the very step after the main
|
| - |
|
444 |
* move, and NOT ONE ever reported aligned. The old refine() hit the same wall and
|
| - |
|
445 |
* silently released. Any loop built on "distance between two detected circles" is
|
| - |
|
446 |
* blind at the only moment that matters.
|
| - |
|
447 |
*
|
| - |
|
448 |
* So the target is fixed ONCE, from the detection already done while the two are far
|
| - |
|
449 |
* apart and resolvable, and from then on we track the RING'S OWN ELEMENT, which stays
|
| - |
|
450 |
* readable however close it gets. Verified locally against the live widget: the ring's
|
| - |
|
451 |
* rect tracks the drag cleanly the whole way in (+10 mouse -> +11 ring, repeatably).
|
| 429 |
*
|
452 |
*
|
| 430 |
* The old path derived px-per-px ONCE from a single probe and then trusted it for
|
453 |
* The circle x's are already CSS px relative to the bg element -- the crop in
|
| 431 |
* the main move and the correction alike, so one noisy probe poisoned everything
|
454 |
* getMatCircles2 uses CSS coordinates and chrome runs with
|
| 432 |
* downstream. Here every move is also a measurement: after each one the ratio is
|
- |
|
| 433 |
* re-derived from what that move actually achieved, so the estimate improves as the
|
455 |
* --force-device-scale-factor=1 -- so the only conversion needed is the bg offset.
|
| 434 |
* piece approaches instead of being fixed at its least informed point.
|
- |
|
| 435 |
*
|
456 |
*
|
| 436 |
* It also gets to see overshoot, which a single pass cannot. The gap is |c1 - c2|,
|
457 |
* Every move is also a measurement: the ratio is re-derived from what that move
|
| 437 |
* so it cannot tell "not there yet" from "gone past" -- but if a move makes the gap
|
458 |
* actually achieved, and it is signed, so overshoot corrects itself instead of needing
|
| 438 |
* GROW, we crossed the hole, and the sign of the next step flips.
|
459 |
* to be detected.
|
| 439 |
*
|
460 |
*
|
| 440 |
* @return total slider px moved, or 0 if the circles could not be read at all.
|
461 |
* @return total slider px moved, or 0 if the ring could not be located at all.
|
| 441 |
*/
|
462 |
*/
|
| 442 |
private int aim(WebDriver driver, Actions actionProvider, double gap) throws Exception {
|
463 |
private int aim(WebDriver driver, Actions actionProvider, double[] circleXs) throws Exception {
|
| - |
|
464 |
Double piece = pieceCentreX(driver);
|
| - |
|
465 |
if (piece == null) {
|
| - |
|
466 |
LOGGER.warn("Aim: no ring element to track");
|
| - |
|
467 |
return 0;
|
| - |
|
468 |
}
|
| - |
|
469 |
int bgX;
|
| - |
|
470 |
try {
|
| - |
|
471 |
bgX = driver.findElement(By.xpath("//*[starts-with(@id, 'dx_captcha_basic_bg_')]")).getLocation().getX();
|
| - |
|
472 |
} catch (Exception e) {
|
| - |
|
473 |
LOGGER.warn("Aim: bg element gone - " + e.getMessage());
|
| - |
|
474 |
return 0;
|
| - |
|
475 |
}
|
| - |
|
476 |
|
| - |
|
477 |
// Of the two detected circles the ring is whichever sits at the ring element; the
|
| - |
|
478 |
// other one is the hole we are aiming at.
|
| - |
|
479 |
double a = bgX + circleXs[0], b = bgX + circleXs[1];
|
| - |
|
480 |
double target = (Math.abs(a - piece) <= Math.abs(b - piece)) ? b : a;
|
| - |
|
481 |
LOGGER.info("Aim: ring at {} target at {} ({}px to close)",
|
| - |
|
482 |
Math.round(piece), Math.round(target), Math.round(target - piece));
|
| - |
|
483 |
|
| 443 |
double ratio = 0; // slider px per gap px, derived from real moves
|
484 |
double ratio = 0; // ring px per mouse px, signed, derived from real moves
|
| 444 |
int total = 0;
|
485 |
int total = 0;
|
| 445 |
int step = SEED_NUDGE;
|
486 |
int step = SEED_NUDGE;
|
| 446 |
|
487 |
|
| 447 |
for (int i = 0; i < MAX_AIM_STEPS; i++) {
|
488 |
for (int i = 0; i < MAX_AIM_STEPS; i++) {
|
| 448 |
actionProvider.moveByOffset(step, 0).perform();
|
489 |
actionProvider.moveByOffset(step, 0).perform();
|
| 449 |
total += step;
|
490 |
total += step;
|
| 450 |
|
491 |
|
| 451 |
double[] c = getMatCircles2(driver, "/tmp/" + Thread.currentThread().getName() + "-aim" + i + ".png");
|
492 |
Double now = pieceCentreX(driver);
|
| 452 |
if (c == null) {
|
493 |
if (now == null) {
|
| 453 |
LOGGER.warn("Aim step {}: lost the circles after {}px", i, total);
|
494 |
LOGGER.warn("Aim step {}: lost the ring after {}px", i, total);
|
| 454 |
return total;
|
495 |
return total;
|
| 455 |
}
|
496 |
}
|
| 456 |
double now = Math.abs(c[0] - c[1]);
|
- |
|
| 457 |
double closed = gap - now;
|
497 |
double moved = now - piece;
|
| 458 |
|
- |
|
| 459 |
if (closed != 0) {
|
498 |
if (Math.abs(moved) > 0.5) {
|
| 460 |
// What this move actually bought us, in slider px per gap px.
|
- |
|
| 461 |
double observed = Math.abs(step) / Math.abs(closed);
|
499 |
double observed = moved / step;
|
| 462 |
// Blend once an estimate exists, so a single bad frame cannot swing it.
|
- |
|
| 463 |
ratio = (ratio <= 0) ? observed : (ratio + observed) / 2;
|
500 |
ratio = (ratio == 0) ? observed : (ratio + observed) / 2;
|
| 464 |
}
|
501 |
}
|
| - |
|
502 |
double remaining = target - now;
|
| 465 |
LOGGER.info("Aim step {}: moved {}px, gap {} -> {}, ratio now {}",
|
503 |
LOGGER.info("Aim step {}: mouse {}px -> ring moved {}px, {}px left, ratio {}",
|
| 466 |
i, step, gap, now, String.format("%.2f", ratio));
|
504 |
i, step, Math.round(moved), Math.round(remaining), String.format("%.2f", ratio));
|
| 467 |
|
505 |
|
| 468 |
if (now <= ALIGNED_PX) {
|
506 |
if (Math.abs(remaining) <= ALIGNED_PX) {
|
| 469 |
LOGGER.info("Aim: aligned within {}px after {} step(s), {}px total", ALIGNED_PX, i + 1, total);
|
507 |
LOGGER.info("Aim: aligned to {}px after {} step(s), {}px of slider",
|
| - |
|
508 |
Math.round(remaining), i + 1, total);
|
| 470 |
return total;
|
509 |
return total;
|
| 471 |
}
|
510 |
}
|
| - |
|
511 |
piece = now;
|
| 472 |
if (ratio <= 0) { // nothing measurable yet, probe again
|
512 |
if (ratio == 0) { // nothing measurable yet, probe again
|
| 473 |
gap = now;
|
- |
|
| 474 |
step = SEED_NUDGE;
|
513 |
step = SEED_NUDGE;
|
| 475 |
continue;
|
514 |
continue;
|
| 476 |
}
|
515 |
}
|
| 477 |
|
- |
|
| 478 |
int next = (int) Math.round(now * ratio);
|
516 |
int next = (int) Math.round(remaining / ratio);
|
| 479 |
if (next == 0) next = 1;
|
517 |
if (next == 0) next = (remaining > 0 ? 1 : -1);
|
| 480 |
if (next > MAX_STEP) next = MAX_STEP;
|
518 |
if (next > MAX_STEP) next = MAX_STEP;
|
| 481 |
// Gap grew => we are past the hole; go back instead of further away.
|
- |
|
| 482 |
step = (closed < 0) ? -next : next;
|
519 |
if (next < -MAX_STEP) next = -MAX_STEP;
|
| 483 |
gap = now;
|
520 |
step = next;
|
| 484 |
}
|
521 |
}
|
| 485 |
LOGGER.info("Aim: {} steps used, {}px total", MAX_AIM_STEPS, total);
|
522 |
LOGGER.info("Aim: {} steps used, {}px of slider, still short", MAX_AIM_STEPS, total);
|
| 486 |
return total;
|
523 |
return total;
|
| 487 |
}
|
524 |
}
|
| 488 |
|
525 |
|
| 489 |
|
- |
|
| 490 |
|
- |
|
| 491 |
private Mat getMatCircles(WebDriver driver, String fileName) throws Exception {
|
526 |
private Mat getMatCircles(WebDriver driver, String fileName) throws Exception {
|
| 492 |
|
527 |
|
| 493 |
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
|
528 |
File screenshot = ((TakesScreenshot) driver).getScreenshotAs(OutputType.FILE);
|
| 494 |
BufferedImage fullImg = ImageIO.read(screenshot);
|
529 |
BufferedImage fullImg = ImageIO.read(screenshot);
|
| 495 |
|
530 |
|