class Mapping(friend_map_ui.Ui_MainWindow): def __init__(self, window, zoom=12): super().__init__() self.setupUi(window) # input: tuple of starting coordinates self.app = QtWidgets.QApplication(sys.argv) # dataframe header names for better referencing self.addr_name = 'Address' self.lat_name = 'Lat' self.lon_name = 'Lon' self.start_lat = 38.6268039 self.start_lon = -90.1994097 self.geo_locator = Geo() self.map = folium.Map(location=(self.start_lat, self.start_lon), zoom_start=zoom) self.default_map_save = path.dirname( path.abspath(__file__)) + r"\map.html" self.address_pickle_location = path.dirname( path.abspath(__file__)) + r"\cities_dictionary.pkl" self.city_lookup_dict = None self.save_location = None self.map_save_path = None self.unsaved_changes = False self.editing_cells = False self.last_edited_data = None self.data = pd.DataFrame( { # will only be used for passthrough to model 'Name': ['Eric Chung'], # type(str) self.addr_name: ['Saint Louis, MO'], # type(str) 'Tags': ['Me, Myself, I'], # type(list) self.lat_name: str(self.start_lat), # type(float) self.lon_name: str(self.start_lon), # type(float) 'Last visited': [datetime.now().strftime('%Y-%m-%d %H:%M:%S')], # type(float) 'Dates visited': ['All the time'], # type(list) 'Date added': [ datetime.fromtimestamp(777186000).strftime( '%Y-%m-%d %H:%M:%S') ], # type(float) 'Description': ['0'] # type(str) }) self._setup_map() self._setup_column_delegates() self._setup_connections() self._setup_column_width_rules() self._load_address_dict_from_pickle() self.stackedWidget_main.setCurrentIndex(0) def add_person(self): # look into vincent and altair for displaying people data whene marker is clicked # https://github.com/wrobstory/vincent # https://altair-viz.github.io/ # for more info on popups # https://nbviewer.jupyter.org/github/python-visualization/folium/blob/master/examples/Popups.ipynb self.unsaved_changes = True ex = popup_widgets.Ui_AddPersonWidget(list(self.data.keys())) info = ex.exec_() if not info: return elif info['Name'] in self.data['Name']: self.print_output( f"{info['name']} already exists. No new entry will be added") return elif not info['Name']: self.print_output( "No name found. Entering a name is required to add entry.") return if info[self.addr_name] and ('Lat' not in info.keys() or 'Lon' not in info.keys()): info[self.lat_name], info[ self.lon_name] = self._get_coordinates_from_address( info[self.addr_name]) elif 'Address' not in info.keys() and info['Lat'] and info['Lon']: info[self.addr_name] = self._get_address_from_coordinates( lat=info[self.lat_name], lon=info[self.lon_name]) df_info = pd.Series(info) self.data = self.data.append( df_info, ignore_index=True) # data frames don't append in place self._update_table_view(info) self.print_output(f"{info['Name']} added!") def add_table_row(self): self._update_table_view(pd.Series()) self.tableView_data.model().layoutChanged.emit() # self.tableView_data.update() def find_and_remove_person(self): print(self.tableView_data.currentIndex().row()) self.tableView_data.model().removeRow( self.tableView_data.currentIndex().row()) # finds selected person and removes from list # https://stackoverflow.com/questions/18172851/deleting-dataframe-row-in-pandas-based-on-column-value def remove_person(self, name): # scan through points dictionary for person or scan through excel file for person??????? self.unsaved_changes = True if name not in self.data['Name']: print(f"{name} not found.") return drop_index = self.data['Name'].index(name) self.data.drop([drop_index]) # TODO def save_map(self, new_save=False): if new_save or not self.map_save_path: self.map_save_path = self.get_save_as() if not self.map_save_path: return self.map.save(self.map_save_path) def display_map(self): self.render_map() self.stackedWidget_main.setCurrentIndex(1) def render_map(self): # # if not self.map.... # # if no map, make new one. If map, check what markers have been added and only add new ones self.map = folium.Map(location=[self.start_lat, self.start_lon], tiles="OpenStreetMap", zoom_start=12) for p_dict in self.tableView_data.model().dataFrame.to_dict('records'): text = self.make_html_popup_text(p_dict) folium.Marker([float(p_dict['Lat']), float(p_dict['Lon'])], popup=text, tooltip=p_dict['Name']).add_to(self.map) self.map.save(self.default_map_save) self.webKit_map.load(QtCore.QUrl.fromLocalFile(self.default_map_save)) def make_html_popup_text(self, info): # input: info = dictionary of all data to display # out: html string popup_text = f"<b></b>{info['Name']}\n\n" for key in info.keys(): if key != 'Name': popup_text += f"<b>{key}:</b> {info[key]}\n\n" return popup_text def export_to_excel(self, new_save=False): if new_save or not self.save_location: self.save_location = self.get_save_as("Excel File (*.xlsx)") if not self.save_location: return self.tableView_data.model().dataFrame.to_excel(self.save_location, index=None, header=True) self.unsaved_changes = False self.print_output("Saved!") #Todo def import_from_excel(self): self.unsaved_changes = True if self.data[ 'Name'] and self.unsaved_changes: # TODO: Add flag for unsaved new = QtWidgets.QMessageBox() reply = QtWidgets.QMessageBox.question( new, "Unsaved data found", "Unsaved data found, would you like to merge with imported file?", QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel) if reply == QtWidgets.QMessageBox.Cancel: return import_file_path = self.get_save_as("Excel File (*.xlsx, *.xls)") if reply == QtWidgets.QMessageBox.Yes: import_data = pd.read_excel(import_file_path) import_names = import_data['Name'].tolist() data_names = self.data['Name'].tolist() for common_name in list( set(import_names).intersection(data_names)): import_name_index = import_names.index(common_name) data_name_index = data_names.index(common_name) prompt_text = f"{common_name} found in both data sets. Which would you like to keep? \n " \ f"Existing Person: {self.data.loc[data_name_index]}\n " \ f"Import Data: {import_data.loc[import_name_index]}" choice = self.query_import_data_question(prompt_text) if choice == 'import': self.data.drop([data_name_index]) else: import_data.drop([import_name_index]) self.data = pd.merge(self.data, import_data, on='Name', how='outer') # TODO: also need to check column order elif reply == QtWidgets.QMessageBox.No: self.data = pd.read_excel(import_file_path) #TODO check this, this was originally copied from other file def find_nearest_friends(self, lon=None, lat=None, city=None, distance=100): if not lon and not lat and not city: return False if (not lon or not lat) and city: lon, lat = self.geo_locator.address_to_lat_lon(city) # ordered = heap() # for i in self.friends: # if dist <= distance: # ordered.add(dist, friend) def find_nearby(self): ex = popup_widgets.Ui_FindNearbyWidget() info = ex.exec_() if not info or not info['lat'] or info['lon']: return if self.tableView_data.model().rowCount() > info['num people']: self.print_output( "Selected number of people is greater than number of people in data..." ) nearby_heap = [] start_coord = (info['lat'], info['lon']) for row in range(self.tableView_data.model().rowCount()): person = self.tableView_data.model().dataFrame.iloc[row] end_coord = (person[self._get_lat_col_index()], person[self._get_lon_col_index()]) distance = self.geo_locator.get_distance(start_coord, end_coord).kilometers pair = (distance, person[0]) if len(nearby_heap) < info['num people']: heapq.heappush(nearby_heap, pair) else: # assuming max heap size now heapq.heappushpop(nearby_heap, pair) #n nearby people calculated... for i in nearby_heap: text = f"{i[1]} is {i[0]}km away." self.print_output(text) def set_home(self): ex = popup_widgets.Ui_SetHomeWidget() info = ex.exec_() if not info: return self.start_lat = info['Lat'] self.start_lon = info['Lon'] def resizeColumnsToContents(self): self.tableView_data.resizeColumnsToContents() def check_location(self, index=None): '''check index check if lat lon changed or city updated other''' if self.editing_cells or not index or self._get_cell_data( index.row(), index.column()) == 'nan': return False self.editing_cells = True if index.column() in (self._get_lat_col_index(), self._get_lon_col_index()): lat_data = self._get_cell_data(index.row(), self._get_lat_col_index()) lon_data = self._get_cell_data(index.row(), self._get_lon_col_index()) address = self._get_address_from_coordinates(lat_data, lon_data) self.set_cell_data(index.row(), self._get_addr_col_index(), address) elif index.column() == self._get_addr_col_index(): print(self._get_cell_data(index.row(), self._get_addr_col_index())) lat, lon = self._get_coordinates_from_address( self._get_cell_data(index.row(), self._get_addr_col_index())) self.set_cell_data(index.row(), self._get_lat_col_index(), lat) self.set_cell_data(index.row(), self._get_lon_col_index(), lon) self.editing_cells = False return True def set_cell_data(self, row, column, data): self.tableView_data.model().dataFrame.iloc[row][column] = data return True def print_output(self, text): self.textEdit_output.moveCursor( QtGui.QTextCursor.End ) # make sure cursor is at end before pasting new text text = f"<b>[{datetime.now().strftime('%H:%M:%S.%f')[:-3]}]:</b> {str(text)}" self.textEdit_output.insertHtml(text) self.textEdit_output.append("") self.textEdit_output.scrollToAnchor(text) self.textEdit_output.moveCursor(QtGui.QTextCursor.End) QtWidgets.QApplication.processEvents() def _get_address_from_coordinates(self, lat, lon): return self.geo.locator.lat_lon_to_address(lat, lon) def _get_coordinates_from_address(self, address): return self.geo_locator.address_to_lat_lon(address) def _get_cell_data(self, row, column): return self.tableView_data.model().dataFrame.iloc[row][column] def _setup_map(self): # added since qt creator doesn't have webengineview built in self.webKit_map = QtWebEngineWidgets.QWebEngineView(self.page_map) sizePolicy = QtWidgets.QSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHorizontalStretch(0) sizePolicy.setVerticalStretch(0) sizePolicy.setHeightForWidth( self.webKit_map.sizePolicy().hasHeightForWidth()) self.webKit_map.setSizePolicy(sizePolicy) self.webKit_map.setObjectName("webKit_map") self.gridLayout_3.addWidget(self.webKit_map, 0, 1, 2, 1) def _setup_column_delegates(self): pandas_table_model = PandasModel(self.data) self.tableView_data.setModel(pandas_table_model) self.lat_delegate = cellValidationDelegate(max=90, min=-90) self.lon_delegate = cellValidationDelegate(max=180, min=-180) self.tableView_data.setItemDelegateForColumn(self._get_lat_col_index(), self.lat_delegate) self.tableView_data.setItemDelegateForColumn(self._get_lon_col_index(), self.lon_delegate) def _setup_column_width_rules(self): header = self.tableView_data.horizontalHeader() for column in range(len(self.data.keys()) - 1): header.setSectionResizeMode(column, QtWidgets.QHeaderView.Interactive) header.setStretchLastSection(True) self.resizeColumnsToContents() def _setup_connections(self): self.pushButton_add_person.clicked.connect(self.add_person) self.pushButton_remove_person.clicked.connect( self.find_and_remove_person) self.pushButton_import.clicked.connect(self.import_from_excel) self.pushButton_save_data.clicked.connect( lambda: self.export_to_excel(False)) self.pushButton_save_data_as.clicked.connect( lambda: self.export_to_excel(True)) self.pushButton_save_map.clicked.connect(lambda: self.save_map(False)) self.pushButton_save_map_as.clicked.connect( lambda: self.save_map(True)) self.pushButton_set_home.clicked.connect(self.set_home) self.pushButton_show_map.clicked.connect(self.display_map) self.pushButton_return_to_main.clicked.connect(self._return_to_main) self.pushButton_add_row.clicked.connect(self.add_table_row) self.tableView_data.model().dataChanged.connect(self.check_location) self.pushButton_find_nearby.clicked.connect(self.find_nearby) def _load_address_dict_from_pickle(self): with open(self.address_pickle_location, 'rb') as f: self.city_lookup_dict = pickle.load(f) def _get_lat_col_index(self): return self.tableView_data.model().dataFrame.columns.tolist().index( self.lat_name) def _get_lon_col_index(self): return self.tableView_data.model().dataFrame.columns.tolist().index( self.lon_name) def _get_addr_col_index(self): return self.tableView_data.model().dataFrame.columns.tolist().index( self.addr_name) def _return_to_main(self): self.stackedWidget_main.setCurrentIndex(0) return True def _update_table_view(self, data): # called when add person is finished row_position = self.tableView_data.model().rowCount() self.tableView_data.model().insertRow(row_position, data=data) def _no_cell_edited(self, index): return self.tableView_data.model().last_edited == self._get_cell_data( index.row(), index.column()) @staticmethod def query_import_data_question(question): message = QtWidgets.QMessageBox() message.setText(question) keep_default = message.addButton('Keep Present Name', QtWidgets.QMessageBox.YesRole) keep_import = message.addButton('Keep Import Name', QtWidgets.QMessageBox.NoRole) message.exec_() return 'default' if message.clickedButton( ) == keep_default else 'import' @staticmethod def get_save_as(filetype="HTML File (*.html)"): filedialog = QtWidgets.QFileDialog() return QtWidgets.QFileDialog.getSaveFileName(filedialog, 'Save As', '', filetype)[0]
class Ui_SetHomeWidget(QtWidgets.QDialog): def __init__(self): # in: options is type(list) of names for each option to input QtWidgets.QDialog.__init__(self) self.info = {} self.geo_locator = Geo() self.setupUi() def setupUi(self): grid = QtWidgets.QGridLayout() grid.setSpacing(5) int_validator = QtGui.QIntValidator() self.city_label = QtWidgets.QLabel("Where are you going?") self.address_input = QtWidgets.QLineEdit() grid.addWidget(self.city_label, 1, 0) grid.addWidget(self.address_input, 1, 1) self.lat_label = QtWidgets.QLabel("Latitude:") self.lat_input = QtWidgets.QSpinBox() grid.addWidget(self.lat_label, 2, 0) grid.addWidget(self.lat_input, 2, 1) self.address_input.editingFinished.connect(self.address_event) self.lat_input.editingFinished.connect(self.lat_lon_event) self.lon_input.editingFinished.connect(self.lat_lon_event) self.double_only = QtGui.QDoubleValidator() self.lat_input.setValidator(self.double_only) self.lon_input.setValidator(self.double_only) self.buttons = QtWidgets.QDialogButtonBox() self.buttons.addButton("Submit", QtWidgets.QDialogButtonBox.AcceptRole) self.buttons.addButton("Cancel", QtWidgets.QDialogButtonBox.RejectRole) grid.addWidget(self.buttons) self.setLayout(grid) # self.setGeometry(300, 300, 300, 300) self.setWindowTitle("Enter Information") self.buttons.accepted.connect(self.submit_close) self.buttons.rejected.connect(self.reject) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.show() def address_event(self): if not self.address_input.text(): return lat, lon = self.geo_locator.address_to_lat_lon( self.address_input.text()) if lat: self.lat_input.setText( lat) # prevents none return from wiping text if lon: self.lon_input.setText(lon) def lat_lon_event(self): if not self.lat_input.text() or not self.lon_input.text(): return address = self.geo_locator.lat_lon_to_address( float(self.lat_input.text()), float(self.lon_input.text())) if address: self.address_input.setText(address) def submit_close(self): self.info['city'] = self.address_input.text() self.info['lat'] = float(self.lat_input.text()) self.info['lon'] = float(self.lon_input.text()) self.accept() def exec_(self): super(Ui_SetHomeWidget, self).exec_() return self.info
class Ui_AddPersonWidget(QtWidgets.QDialog): def __init__(self, options): # in: options is type(list) of names for each option to input QtWidgets.QDialog.__init__(self) self.obj_list = [ ] # storage for dynamic variables since can't name them all..# . self.info = {} self.options = options self.setupUi() self.address_input = self.obj_list[2 * self.options.index('Address') + 1] self.lat_input = self.obj_list[2 * self.options.index('Lat') + 1] self.lon_input = self.obj_list[2 * self.options.index('Lon') + 1] self.geo_locator = Geo() self.address_input.editingFinished.connect(self.address_event) self.lat_input.editingFinished.connect(self.lat_lon_event) self.lon_input.editingFinished.connect(self.lat_lon_event) def setupUi(self): y_cnt = 1 grid = QtWidgets.QGridLayout() grid.setSpacing(5) for i in self.options: name = QtWidgets.QLabel(i) user_input = QtWidgets.QLineEdit() if name.text().lower() == 'date added': user_input.setText(datetime.date.today().strftime("%B %d, %Y")) self.obj_list.append(name) self.obj_list.append(user_input) grid.addWidget(name, y_cnt, 0) grid.addWidget(user_input, y_cnt, 1) y_cnt += 1 self.buttons = QtWidgets.QDialogButtonBox() self.buttons.addButton("Submit", QtWidgets.QDialogButtonBox.AcceptRole) self.buttons.addButton("Cancel", QtWidgets.QDialogButtonBox.RejectRole) grid.addWidget(self.buttons) self.setLayout(grid) self.setGeometry(300, 300, 100 * y_cnt, 300) self.setWindowTitle("Enter Information") self.buttons.accepted.connect(self.submit_close) self.buttons.rejected.connect(self.reject) self.show() def address_event(self): if not self.address_input.text(): return try: lat, lon = self.geo_locator.address_to_lat_lon( self.address_input.text()) except TypeError: print("error, address not recognized, please check spelling") return self.obj_list[2 * self.options.index('Lat') + 1].setText(str(lat)) self.obj_list[2 * self.options.index('Lon') + 1].setText(str(lon)) def lat_lon_event(self): lat, lon = self._get_lat_lon() if not lat or not lon: return address = self.geo_locator.lat_lon_to_address(lat, lon) self.address_input.setText(address) def _get_lat_lon(self): lat = self.lat_input.text() if not self.lat_input.text() else float( self.lat_input.text()) lon = self.lon_input.text() if not self.lon_input.text() else float( self.lon_input.text()) return lat, lon def submit_close(self): #TODO: save input data to self.info first if not self.obj_list[1].text(): print("Name required.") return for index in range(0, len(self.obj_list), 2): self.info[self.obj_list[index].text()] = self.obj_list[index + 1].text() self.accept() def exec_(self): super(Ui_AddPersonWidget, self).exec_() return self.info
class Ui_FindNearbyWidget(QtWidgets.QDialog): def __init__(self): # in: options is type(list) of names for each option to input QtWidgets.QDialog.__init__(self) self.info = {} self.geo_locator = Geo() self.location_coordinates = None self.setupUi() def setupUi(self): grid = QtWidgets.QGridLayout() grid.setSpacing(5) self.city_label = QtWidgets.QLabel( "Where are you located? (Address/City)") self.address_input = QtWidgets.QLineEdit() grid.addWidget(self.city_label, 1, 0) grid.addWidget(self.address_input, 1, 1) self.label_num_people = QtWidgets.QLabel("Number of nearest people:") self.spinBox_num_people = QtWidgets.QSpinBox() grid.addWidget(self.label_num_people, 2, 0) grid.addWidget(self.spinBox_num_people, 2, 1) self.address_input.editingFinished.connect(self.address_event) self.buttons = QtWidgets.QDialogButtonBox() self.buttons.addButton("Submit", QtWidgets.QDialogButtonBox.AcceptRole) self.buttons.addButton("Cancel", QtWidgets.QDialogButtonBox.RejectRole) grid.addWidget(self.buttons) self.setLayout(grid) self.setWindowTitle("Enter Information") self.buttons.accepted.connect(self.submit_close) self.buttons.rejected.connect(self.reject) sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Minimum, QtWidgets.QSizePolicy.Expanding) sizePolicy.setHeightForWidth(self.sizePolicy().hasHeightForWidth()) self.show() def address_event(self): if not self.address_input.text(): return lat, lon = self.geo_locator.address_to_lat_lon( self.address_input.text()) if lat: self.info['lat'] = lat if lon: self.info['lon'] = lon def lat_lon_event(self): if not self.lat_input.text() or not self.lon_input.text(): return address = self.geo_locator.lat_lon_to_address( float(self.lat_input.text()), float(self.lon_input.text())) if address: self.address_input.setText(address) def submit_close(self): self.info['num people'] = self.spinBox_num_people.value() self.accept() def exec_(self): super(Ui_SetHomeWidget, self).exec_() return self.info