Omniscient001/Omniscient
1
1import time2from typing import Dict, Optional, List3 4import undetected_chromedriver as uc5from selenium.webdriver.support.ui import WebDriverWait6from selenium.webdriver.support import expected_conditions as EC7from selenium.webdriver.common.by import By8 9from config import MAPCRUNCH_URL, SELECTORS, DATA_COLLECTION_CONFIG10 11 12class MapCrunchController:13 def __init__(self, headless: bool = False):14 options = uc.ChromeOptions()15 options.add_argument(16 "user-agent=Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"17 )18 options.add_argument("--window-size=1920,1080")19 options.set_capability("goog:loggingPrefs", {"browser": "ALL"})20 21 if headless:22 options.add_argument("--headless=new")23 24 self.driver = uc.Chrome(options=options, use_subprocess=True)25 self.wait = WebDriverWait(self.driver, 10)26 27 # Here we are injecting a script to the page to disable the browser detection.28 # Basically, we are setting the badBrowser property to 0, which is a property that is used to detect if the browser is being controlled by a script.29 # In the main.min.js, we can see some js code like this:30 # if (badBrowser) {31 # alert("Unsupported browser!");32 # } else {33 # window.panorama = { ... }34 # }35 self.driver.execute_cdp_cmd(36 "Page.addScriptToEvaluateOnNewDocument",37 {38 "source": """39 Object.defineProperty(window, 'badBrowser', {40 value: 0,41 writable: false,42 configurable: false43 });44 window.alert = function() {};45 Object.defineProperty(navigator, 'webdriver', {46 get: () => undefined47 });48 """49 },50 )51 52 for retry in range(3):53 try:54 self.driver.get(MAPCRUNCH_URL)55 time.sleep(3)56 break57 except Exception as e:58 if retry == 2:59 raise e60 print(f"Failed to load MapCrunch, retry {retry + 1}/3")61 time.sleep(2)62 63 def setup_clean_environment(self):64 """65 Minimal environment setup using hideLoc() and hiding major UI.66 """67 self.driver.execute_script("if(typeof hideLoc === 'function') hideLoc();")68 self.driver.execute_script("""69 const menu = document.querySelector('#menu');70 if (menu) menu.style.display = 'none';71 72 const social = document.querySelector('#social');73 if (social) social.style.display = 'none';74 75 const googleImg = document.querySelector('img[alt="Google"]');76 if (googleImg && googleImg.parentElement) {77 googleImg.parentElement.style.display = 'none';78 }79 80 const topBar = document.querySelector('#topbar');81 if (topBar) topBar.style.display = 'none';82 83 const bottomBox = document.querySelector('#bottom-box');84 if (bottomBox) bottomBox.style.display = 'none';85 86 const infoFirstView = document.querySelector('#info-firstview');87 if (infoFirstView) infoFirstView.style.display = 'none';88 89 const controlsToHide = document.querySelectorAll('.gm-style-cc'); controlsToHide.forEach(el => { el.style.display = 'none'; });90 const keyboardButton = document.querySelector('button[aria-label="Keyboard shortcuts"]'); if (keyboardButton) { keyboardButton.style.display = 'none'; }91 92 93 """)94 95 def label_arrows_on_screen(self):96 """Overlays 'UP' and 'DOWN' labels on the navigation arrows."""97 try:98 pov = self.driver.execute_script("return window.panorama.getPov();")99 links = self.driver.execute_script("return window.panorama.getLinks();")100 except Exception:101 return102 103 if not links or not pov:104 return105 106 current_heading = pov["heading"]107 forward_link = None108 backward_link = None109 110 # This logic is identical to your existing `move` function111 # to ensure stylistic and behavioral consistency.112 min_forward_diff = 360113 for link in links:114 diff = 180 - abs(abs(link["heading"] - current_heading) - 180)115 if diff < min_forward_diff:116 min_forward_diff = diff117 forward_link = link118 119 target_backward_heading = (current_heading + 180) % 360120 min_backward_diff = 360121 for link in links:122 diff = 180 - abs(abs(link["heading"] - target_backward_heading) - 180)123 if diff < min_backward_diff:124 min_backward_diff = diff125 backward_link = link126 127 js_script = """128 document.querySelectorAll('.geobot-arrow-label').forEach(el => el.remove());129 document.querySelectorAll('path[data-geobot-modified]').forEach(arrow => {130 arrow.setAttribute('transform', arrow.getAttribute('data-original-transform') || '');131 arrow.removeAttribute('data-geobot-modified');132 arrow.removeAttribute('data-original-transform');133 });134 135 const modifyAndLabelArrow = (panoId, labelText, color) => {136 const arrowElement = document.querySelector(`path[pano="${panoId}"]`);137 if (!arrowElement) return;138 139 const originalTransform = arrowElement.getAttribute('transform') || '';140 arrowElement.setAttribute('data-original-transform', originalTransform);141 arrowElement.setAttribute('transform', `${originalTransform} scale(1.8)`);142 arrowElement.setAttribute('data-geobot-modified', 'true');143 144 const rect = arrowElement.getBoundingClientRect();145 const label = document.createElement('div');146 label.className = 'geobot-arrow-label';147 label.style.position = 'fixed';148 label.style.left = `${rect.left + rect.width / 2}px`;149 label.style.top = `${rect.top - 45}px`;150 label.style.transform = 'translateX(-50%)';151 label.style.padding = '5px 15px';152 label.style.backgroundColor = 'rgba(0, 0, 0, 0.8)';153 label.style.color = color;154 label.style.borderRadius = '8px';155 label.style.fontSize = '28px';156 label.style.fontWeight = 'bold';157 label.style.zIndex = '99999';158 label.style.pointerEvents = 'none';159 label.innerText = labelText;160 document.body.appendChild(label);161 };162 163 const forwardPano = arguments[0];164 const backwardPano = arguments[1];165 166 if (forwardPano) {167 modifyAndLabelArrow(forwardPano, 'UP', '#76FF03');168 }169 if (backwardPano && backwardPano !== forwardPano) {170 modifyAndLabelArrow(backwardPano, 'DOWN', '#F44336');171 }172 """173 174 forward_pano = forward_link["pano"] if forward_link else None175 backward_pano = backward_link["pano"] if backward_link else None176 177 self.driver.execute_script(js_script, forward_pano, backward_pano)178 time.sleep(0.2)179 180 def get_available_actions(self) -> List[str]:181 """182 Checks for movement links via JavaScript.183 """184 base_actions = ["PAN_LEFT", "PAN_RIGHT", "GUESS"]185 links = self.driver.execute_script("return window.panorama.getLinks();")186 if links and len(links) > 0:187 base_actions.extend(["MOVE_FORWARD", "MOVE_BACKWARD"])188 return base_actions189 190 def get_current_address(self) -> Optional[str]:191 try:192 address_element = self.wait.until(193 EC.visibility_of_element_located(194 (By.CSS_SELECTOR, SELECTORS["address_element"])195 )196 )197 address_text = address_element.text.strip()198 address_title = address_element.get_attribute("title") or ""199 return (200 address_title201 if len(address_title) > len(address_text)202 else address_text203 )204 except Exception:205 return "Stealth Mode"206 207 def pan_view(self, direction: str, degrees: int = 45):208 """Pans the view using a direct JS call."""209 pov = self.driver.execute_script("return window.panorama.getPov();")210 if direction == "left":211 pov["heading"] -= degrees212 elif direction == "right":213 pov["heading"] += degrees214 self.driver.execute_script("window.panorama.setPov(arguments[0]);", pov)215 time.sleep(0.5)216 217 def move(self, direction: str):218 """Moves by finding the best panorama link and setting it via JS."""219 pov = self.driver.execute_script("return window.panorama.getPov();")220 links = self.driver.execute_script("return window.panorama.getLinks();")221 if not links:222 return223 224 current_heading = pov["heading"]225 best_link = None226 227 if direction == "forward":228 min_diff = 360229 for link in links:230 diff = 180 - abs(abs(link["heading"] - current_heading) - 180)231 if diff < min_diff:232 min_diff = diff233 best_link = link234 elif direction == "backward":235 target_heading = (current_heading + 180) % 360236 min_diff = 360237 for link in links:238 diff = 180 - abs(abs(link["heading"] - target_heading) - 180)239 if diff < min_diff:240 min_diff = diff241 best_link = link242 243 if best_link:244 self.driver.execute_script(245 "window.panorama.setPano(arguments[0]);", best_link["pano"]246 )247 time.sleep(2.5)248 249 def select_map_location_and_guess(self, lat: float, lon: float):250 """Minimalist guess confirmation."""251 self.driver.execute_script(252 "document.querySelector('#bottom-box').style.display = 'block';"253 )254 self.wait.until(255 EC.element_to_be_clickable((By.CSS_SELECTOR, SELECTORS["go_button"]))256 ).click()257 time.sleep(0.5)258 self.wait.until(259 EC.element_to_be_clickable((By.CSS_SELECTOR, SELECTORS["confirm_button"]))260 ).click()261 time.sleep(3)262 263 def get_ground_truth_location(self) -> Optional[Dict[str, float]]:264 """Directly gets location from JS object."""265 return self.driver.execute_script("return window.loc;")266 267 def click_go_button(self) -> bool:268 self.wait.until(269 EC.element_to_be_clickable((By.CSS_SELECTOR, SELECTORS["go_button"]))270 ).click()271 time.sleep(DATA_COLLECTION_CONFIG.get("wait_after_go", 3))272 return True273 274 def take_street_view_screenshot(self) -> Optional[bytes]:275 pano_element = self.wait.until(276 EC.presence_of_element_located(277 (By.CSS_SELECTOR, SELECTORS["pano_container"])278 )279 )280 return pano_element.screenshot_as_png281 282 def load_location_from_data(self, location_data: Dict) -> bool:283 pano_id, pov = location_data.get("pano_id"), location_data.get("pov")284 if pano_id and pov:285 self.driver.execute_script(286 "window.panorama.setPano(arguments[0]); window.panorama.setPov(arguments[1]);",287 pano_id,288 pov,289 )290 time.sleep(2)291 return True292 return False293 294 def close(self):295 if self.driver:296 self.driver.quit()297 298 def __enter__(self):299 return self300 301 def __exit__(self, exc_type, exc_val, exc_tb):302 self.close()303 304 def load_url(self, url):305 """Load a specific MapCrunch URL."""306 try:307 self.driver.get(url)308 time.sleep(2) # Wait for the page to load309 return True310 except Exception as e:311 print(f"Error loading URL: {e}")312 return False313 