Combine the following two scripts and improve whatever you can. import osmnx as ox
import geopandas as gpd
import networkx as nx
from shapely.geometry import LineString

class FiberNetworkDesign:
    def __init__(self):
        pass

    def log_message(self, message):
        """Logs the current status of the script for debugging purposes."""
        print(message)

    def buffer_road_network(self, graph, buffer_distance=6.5):
        """Buffer road network edges by the specified distance (6.5 meters)."""
        self.log_message("Buffering road network to generate fiber trench areas...")

        # Convert road edges to LineStrings
        edges = ox.graph_to_gdfs(graph, nodes=False)

        # Apply buffer to each edge
        buffered_edges = edges.geometry.buffer(buffer_distance)

        # Convert to a GeoDataFrame
        buffered_gdf = gpd.GeoDataFrame(geometry=buffered_edges, crs=edges.crs)

        self.log_message(f"Buffered {len(buffered_edges)} road segments.")
        return buffered_gdf

    def create_network_lines(self, buffered_gdf):
        """Create network lines from the buffered polygons."""
        self.log_message("Converting buffered polygons to network lines...")

        # Convert buffered polygons to their centroids (representing trench lines)
        centroids = buffered_gdf.centroid

        # Create lines by connecting consecutive centroids
        network_lines = []
        for i in range(len(centroids) - 1):
            line = LineString([centroids.iloc[i], centroids.iloc[i + 1]])
            network_lines.append(line)

        # Convert to GeoDataFrame
        network_lines_gdf = gpd.GeoDataFrame(geometry=network_lines, crs=buffered_gdf.crs)

        self.log_message(f"Created {len(network_lines)} network lines.")
        return network_lines_gdf

    def add_road_crossings(self, network_lines_gdf, intersection_tolerance=10):
        """Add road crossings at intersections to interconnect the network lines."""
        self.log_message("Adding road crossings at intersections...")

        # Find intersections between network lines
        intersections = gpd.overlay(network_lines_gdf, network_lines_gdf, how='intersection')

        # Filter out small intersections (less than intersection_tolerance)
        valid_intersections = intersections[intersections.geometry.length > intersection_tolerance]

        # Add lines connecting the valid intersections (road crossings)
        crossing_lines = []
        for idx, intersection in valid_intersections.iterrows():
            line = LineString(intersection.geometry)
            crossing_lines.append(line)

        # Convert to GeoDataFrame
        crossing_lines_gdf = gpd.GeoDataFrame(geometry=crossing_lines, crs=network_lines_gdf.crs)

        self.log_message(f"Added {len(crossing_lines)} road crossings.")
        return crossing_lines_gdf

    def create_network_graph(self, network_lines_gdf, crossing_lines_gdf):
        """Convert network lines and road crossings into a graph for shortest path calculation."""
        self.log_message("Creating network graph for shortest path calculations...")

        # Initialize an empty graph
        G = nx.Graph()

        # Add edges from the network lines
        for idx, line in network_lines_gdf.iterrows():
            coords = list(line.geometry.coords)
            for i in range(len(coords) - 1):
                G.add_edge(coords[i], coords[i + 1], length=line.geometry.length)

        # Add edges from the crossing lines
        for idx, line in crossing_lines_gdf.iterrows():
            coords = list(line.geometry.coords)
            for i in range(len(coords) - 1):
                G.add_edge(coords[i], coords[i + 1], length=line.geometry.length)

        self.log_message(f"Graph created with {len(G.nodes())} nodes and {len(G.edges())} edges.")
        return G

    def calculate_shortest_paths(self, G, olt_location, customer_locations):
        """Calculate shortest paths and place network components."""
        self.log_message("Calculating shortest paths and adding network components...")

        # Nearest network node to the OLT
        olt_node = ox.distance.nearest_nodes(G, olt_location[1], olt_location[0])

        paths = []
        for customer_coords in customer_locations:
            # Nearest network node to the customer
            customer_node = ox.distance.nearest_nodes(G, customer_coords[0], customer_coords[1])

            # Calculate shortest path using network graph
            shortest_path = nx.shortest_path(G, olt_node, customer_node, weight='length')
            paths.append(shortest_path)

            # Add network components along the path (manholes, splitters, etc.)
            self.add_network_components(shortest_path)

        self.log_message(f"Calculated shortest paths for {len(customer_locations)} customers.")
        return paths

    def add_network_components(self, path):
        """Add manholes, splitters, fiber counts, and other components along the path."""
        manhole_interval = 100  # Example interval in meters
        for i, segment in enumerate(path):
            # Add a manhole at intervals or at key points like intersections
            if i % manhole_interval == 0:
                self.log_message(f"Manhole placed at {segment}.")

            # Add splitters and other components as needed
            # (Customize the logic based on your deployment needs)

    def run_fiber_network_design(self, olt_location, customer_locations, city_name="Porterville"):
        """Main function to run the fiber network design process."""
        self.log_message(f"Starting fiber network design for {city_name}...")

        # 1. Download road network
        self.log_message("Downloading road network...")
        road_graph = ox.graph_from_place(city_name, network_type='drive')

        # 2. Buffer the road network
        buffered_gdf = self.buffer_road_network(road_graph)

        # 3. Create network lines from buffered roads
        network_lines_gdf = self.create_network_lines(buffered_gdf)

        # 4. Add road crossings to interconnect the network
        crossing_lines_gdf = self.add_road_crossings(network_lines_gdf)

        # 5. Create network graph for shortest path routing
        network_graph = self.create_network_graph(network_lines_gdf, crossing_lines_gdf)

        # 6. Calculate shortest paths from OLT to customer locations
        shortest_paths = self.calculate_shortest_paths(network_graph, olt_location, customer_locations)

        self.log_message("Fiber network design process complete.")
        return shortest_paths

# Example usage:
olt_location = (33.012, 18.678)  # Example coordinates for OLT
customer_locations = [
    (33.014, 18.681),
    (33.016, 18.675),
    (33.018, 18.672)
]  # Example customer coordinates

fiber_design = FiberNetworkDesign()
shortest_paths = fiber_design.run_fiber_network_design(olt_location, customer_locations)

import osmnx as ox
import simplekml
import numpy as np
import tkinter as tk
from tkinter import messagebox, ttk, filedialog
import logging
import json
import os
import geopy.distance as geo_dist
import networkx as nx

logging.basicConfig(level=logging.DEBUG, format='%(asctime)s - %(levelname)s - %(message)s')

class NetworkDesignTool:
    def __init__(self, root):
        self.setup_ui(root)
        self.load_preferences()
        self.network_costs = {"fiber_cost_per_meter": 2.5, "trenching_cost_per_meter": 5.0, "manhole_cost": 500}
        self.init_logging()

    def setup_ui(self, root):
        """Setup the user interface components."""
        root.title("Advanced GPON FTTH Network Design Tool")
        
        # Interface for area name
        self.create_label_entry(root, "Area Name:", 0)
        self.area_name_entry = self.entries[-1]

        # Interface for fiber ratio selection
        self.create_label_combobox(root, "Fiber Ratio:", ["1:4", "1:8", "1:16", "1:32"], 1)
        self.fiber_ratio_combobox = self.comboboxes[-1]

        # Interface for deployment option selection
        self.create_label_combobox(root, "Deployment Option:", ["underground", "overhead"], 2)
        self.deployment_option_combobox = self.comboboxes[-1]

        # Interface for costs input
        self.create_label_entry(root, "Fiber Cost (per meter):", 3, "2.5")
        self.fiber_cost_entry = self.entries[-1]
        self.create_label_entry(root, "Trenching Cost (per meter):", 4, "5.0")
        self.trenching_cost_entry = self.entries[-1]

        # Save Preferences checkbox
        self.save_pref_var = tk.BooleanVar()
        tk.Checkbutton(root, text="Save Preferences", variable=self.save_pref_var).grid(row=5, column=0, padx=10, pady=5, columnspan=2)

        # Run Button
        run_button = tk.Button(root, text="Run Network Design", command=self.run_network_design)
        run_button.grid(row=6, column=0, columnspan=2, pady=20)

        # Progress Bar
        self.progress = ttk.Progressbar(root, orient='horizontal', length=250, mode='determinate')
        self.progress.grid(row=7, column=0, columnspan=2, pady=10)

        # Output log textbox
        self.log_output = tk.Text(root, height=12, width=60)
        self.log_output.grid(row=8, column=0, columnspan=2, padx=10, pady=10)
        self.log_output.config(state=tk.DISABLED)

    def create_label_entry(self, root, label_text, row, default_val=""):
        """Create a label and entry widget."""
        tk.Label(root, text=label_text).grid(row=row, column=0, padx=10, pady=5, sticky='e')
        entry = tk.Entry(root, width=30)
        entry.grid(row=row, column=1, padx=10, pady=5)
        if default_val:
            entry.insert(0, default_val)
        if not hasattr(self, 'entries'):
            self.entries = []
        self.entries.append(entry)

    def create_label_combobox(self, root, label_text, values, row):
        """Create a label and combobox widget."""
        tk.Label(root, text=label_text).grid(row=row, column=0, padx=10, pady=5, sticky='e')
        combobox = ttk.Combobox(root, values=values, state="readonly")
        combobox.grid(row=row, column=1, padx=10, pady=5)
        combobox.current(0)
        if not hasattr(self, 'comboboxes'):
            self.comboboxes = []
        self.comboboxes.append(combobox)

    def load_preferences(self):
        """Load user preferences from a JSON file."""
        filepath = "user_preferences.json"
        if os.path.exists(filepath):
            try:
                with open(filepath, "r") as file:
                    preferences = json.load(file)
                    self.area_name_entry.insert(0, preferences.get("area_name", ""))
                    self.fiber_ratio_combobox.set(preferences.get("fiber_ratio", "1:4"))
                    self.deployment_option_combobox.set(preferences.get("deployment_option", "underground"))
            except json.JSONDecodeError as e:
                self.log_message(f"Error loading preferences: {str(e)}", level="error")

    def save_preferences(self):
        """Save user preferences to a JSON file."""
        preferences = {
            "area_name": self.area_name_entry.get(),
            "fiber_ratio": self.fiber_ratio_combobox.get(),
            "deployment_option": self.deployment_option_combobox.get(),
        }
        filepath = "user_preferences.json"
        with open(filepath, "w") as file:
            json.dump(preferences, file, indent=4)

    def init_logging(self):
        """Initialize logging to both console and file."""
        self.logger = logging.getLogger('NetworkDesignTool')
        fh = logging.FileHandler('network_design.log')
        fh.setLevel(logging.DEBUG)
        self.logger.addHandler(fh)

    def log_message(self, message, level="info"):
        """Log messages to the logging output and GUI."""
        self.logger.log(getattr(logging, level.upper()), message)
        self.log_output.config(state=tk.NORMAL)
        self.log_output.insert(tk.END, f"{message}\n")
        self.log_output.config(state=tk.DISABLED)
        self.log_output.see(tk.END)

    def retrieve_data(self, area_name):
        """Retrieve road and building data from OpenStreetMap for the specified area."""
        self.log_message(f"Retrieving data for {area_name}...")
        try:
            graph = ox.graph_from_place(area_name, network_type='drive')
            buildings = ox.features_from_place(area_name, tags={'building': True})
            self.log_message(f"Nodes in graph: {len(graph.nodes)}, Buildings: {len(buildings)}")
            return graph, buildings
        except Exception as e:
            self.log_message(f"Error retrieving data: {str(e)}", level="error")
            return None, None

    def design_network(self, graph, buildings, fiber_ratio):
        """Design the GPON FTTH network by determining OLT and splitter placements."""
        self.log_message("Designing GPON FTTH network...")
        try:
            OLT_location = list(graph.nodes())[0]
            olt_coords = (graph.nodes[OLT_location]['y'], graph.nodes[OLT_location]['x'])
            customer_locations = [building.geometry for _, building in buildings.iterrows()]

            splitters = []
            for i in range(0, len(customer_locations), int(fiber_ratio.split(':')[1])):
                splitter_location = customer_locations[i]
                splitters.append(splitter_location)

            self.log_message(f"OLT placed at {olt_coords}, {len(customer_locations)} customer locations identified.")
            return olt_coords, splitters, customer_locations
        except Exception as e:
            self.log_message(f"Error designing network: {str(e)}", level="error")
            return None, None, None

    def calculate_distances(self, olt_location, customer_locations):
        """Calculate distances from OLT to each customer location."""
        distances = []
        try:
            for customer in customer_locations:
                customer_coords = customer.centroid.coords[0]
                distance = geo_dist.distance(olt_location, customer_coords).m
                distances.append((customer_coords, distance))
            self.log_message(f"Calculated distances for {len(customer_locations)} customers.")
        except Exception as e:
            self.log_message(f"Error calculating distances: {str(e)}", level="error")
        return distances

    def calculate_total_cost(self, distances, splitters):
        """Calculate total cost of the network based on fiber, trenching, and splitter costs."""
        total_cost = 0.0
        fiber_cost_per_meter = float(self.fiber_cost_entry.get())
        trenching_cost_per_meter = float(self.trenching_cost_entry.get())
        
        try:
            for _, distance in distances:
                total_cost += distance * (fiber_cost_per_meter + trenching_cost_per_meter)
            
            total_cost += len(splitters) * self.network_costs['manhole_cost']
            self.log_message(f"Estimated total cost: ${total_cost:.2f}")
        except Exception as e:
            self.log_message(f"Error calculating total cost: {str(e)}", level="error")
        return total_cost

    def generate_kml(self, olt_location, splitters, customer_locations, distances, deployment_option):
        """Generate a KML file representing the GPON network."""
        self.log_message("Generating KML file...")
        try:
            kml = simplekml.Kml()

            # OLT point
            kml.newpoint(name="OLT", coords=[(olt_location[1], olt_location[0])])

            # Lines to splitters
            splitter_coords = []
            for i, splitter in enumerate(splitters):
                coords = splitter.centroid.coords[0]
                splitter_coords.append(coords)
                line = kml.newlinestring(name=f"Feeder Line to Splitter {i+1}",
                                         coords=[(olt_location[1], olt_location[0]), (coords[0], coords[1])])
                line.style.linestyle.color = simplekml.Color.red
                line.style.linestyle.width = 2

            # Lines to customers
            for i, (customer_coords, distance) in enumerate(distances):
                if splitter_coords:
                    coords = splitter_coords[i // int(fiber_ratio.split(':')[1])]
                    line = kml.newlinestring(name=f"Drop Line to Customer {i+1}",
                                             coords=[(coords[0], coords[1]), (customer_coords[0], customer_coords[1])])
                    line.style.linestyle.color = simplekml.Color.blue if deployment_option == 'overhead' else simplekml.Color.green
                    line.style.linestyle.width = 1

            kml.save("GPON_Network_Design.kml")
            self.log_message("KML file saved as GPON_Network_Design.kml.")
        except Exception as e:
            self.log_message(f"Error generating KML: {str(e)}", level="error")

    def run_network_design(self):
        """Main function to execute the network design workflow."""
        area_name = self.area_name_entry.get()
        fiber_ratio = self.fiber_ratio_combobox.get()
        deployment_option = self.deployment_option_combobox.get()

        if self.save_pref_var.get():
            self.save_preferences()

        self.progress.start(10)

        try:
            graph, buildings = self.retrieve_data(area_name)
            if graph is None or buildings is None:
                self.log_message("Failed to retrieve necessary data for network design.", level="error")
                return

            olt_coords, splitters, customer_locations = self.design_network(graph, buildings, fiber_ratio)
            if olt_coords is None:
                self.log_message("Failed to design network.", level="error")
                return
            
            distances = self.calculate_distances(olt_coords, customer_locations)
            total_cost = self.calculate_total_cost(distances, splitters)
            self.generate_kml(olt_coords, splitters, customer_locations, distances, deployment_option)
            
        except Exception as e:
            self.log_message(f"Error during network design: {str(e)}", level="error")
        finally:
            self.progress.stop()

if __name__ == "__main__":
    root = tk.Tk()
    app = NetworkDesignTool(root)
    root.mainloop()