# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python (Cfox2)
#     language: python
#     name: cfox2
# ---

import sys, os
import geopandas as gpd
import glob2
import os
import io
import zipfile
import pandas as pd
from shapely.geometry import Point, LineString, MultiLineString
from scipy import spatial
from functools import partial
import pyproj
from shapely.ops import transform
import re
from shapely.wkt import loads

# ### Setup
# Point the script to location containing all of the data

data_location = r'C:\Users\charl\Documents\GOST\Vietnam\unzipped'

# ### Diagnosis: Zipped Data
# Here we try to identify what we are dealing with by iterating through all paths and subfolders, and summarizing the number of files / folders and their extensions

all_paths = glob2.glob(data_location+r'\**')
print('separate objects in folder tree: %s' % len(all_paths))


def PathDrDiagnose(path, verbose = False):
    
    print('for path %s:' % path)
    
    zipped_paths, csv_paths, kml_paths, rar_paths, others, file_extensions = [],[],[],[],[],[]
    
    all_paths = glob2.glob(path+r'\**')
    for z in all_paths:
        if z[-4:] == '.zip':
            zipped_paths.append(z)
        elif z[-4:] == '.csv':
            csv_paths.append(z)
        elif z[-4:] == '.kml':
            kml_paths.append(z)
        elif z[-4:] == '.rar':
            rar_paths.append(z)
        else:
            others.append(z)

        extension = os.path.splitext(z)[1]

        if len(extension) < 6:
                file_extensions.append(extension)

    df = pd.DataFrame({'extensions':file_extensions})

    print('zipped paths: %s' % len(zipped_paths))
    print('csv files: %s' % len(csv_paths))
    print('kml files: %s' % len(kml_paths))
    print('rar folders: %s' % len(rar_paths))
    if verbose == True:
        print(df.extensions.value_counts().to_frame())
    return rar_paths


rar_paths = PathDrDiagnose(data_location)


# ### Unzipping
# Generate new folders for all zipped paths and nested zipped paths (zipped folders inside zipped folders...)

def PathDrUnzip(in_path, unzip_path = None, delete = True, collapse=False, collapseDepth=5):
    
    if unzip_path == None:
        unzip_path = in_path
    
    error_counter = 0
    zipped_paths = []
    all_paths = glob2.glob(in_path+r'\**')
    
    for z in all_paths:
        if z[-4:] == '.zip':
            zipped_paths.append(z)
    
    def unzip(pth, in_path, unzip_path, delete = True):
        error_counter = 0
        subzip = []
        
        try:
            with zipfile.ZipFile(pth) as z:
                dirname = os.path.splitext(pth)[0]
                if os.path.exists(dirname):
                    pass
                else:
                    if collapse == True:
                        middle = os.path.relpath(pth, in_path)
                        middle = middle.split('\\')[:collapseDepth]
                        loc = os.path.join(unzip_path, *middle[:-1])
                    else:
                        loc = os.path.join(unzip_path, os.path.splitext(os.path.relpath(pth, in_path))[0])
                    if os.path.exists(loc):
                        print('passing - %s exists' % loc)
                        pass
                    else:
                        os.mkdir(loc)
                    for i in z.namelist():
                        if i[-4:] == '.zip':
                            print('adding subzip: ',os.path.join(loc, i))
                            subzip.append(os.path.join(loc, i))
                        print(i)
                        z.extract(i, loc)

            if delete == True: 
                os.unlink(pth)
        
        except:
            print('ERROR: cannot unzip %s' % pth)
            error_counter += 1
         
        return subzip, error_counter

    for zippy in zipped_paths:
        
        print('unzipping %s' % zippy)
        remnants, new_errors = unzip(zippy, in_path, unzip_path, delete)
        error_counter += new_errors
        
        while len(remnants) > 0:    
            
            for sub_zippy in remnants:
                
                print('unzipping sub-zip: %s' % sub_zippy)
                res, new_errors = unzip(sub_zippy, in_path, unzip_path, delete)
                remnants.remove(sub_zippy)
                error_counter += new_errors
                
                if len(res) > 0:
                    
                    print('further subfolders found')
                    remnants.append(*res)
                    
    print('errors encountered: %s' % error_counter)


# ### Execute Unzipping

# +
test_path = r'C:\Users\charl\Documents\GOST\Vietnam\unzipped'

print('pre unzip:')
PathDrDiagnose(test_path)

PathDrUnzip(test_path, test_path, delete = True, collapse=True, collapseDepth=2)

print('\npost unzip:')
PathDrDiagnose(test_path)
# -

# ### Diagnosis: Unzipped Data

# +
road_summaries, intervals, roadpaths, roughness, paths = [], [], [], [], []

path = data_location
all_paths = glob2.glob(path+r'\**')
for z in all_paths:
    if 'RoadSummary' in z and '.csv' in z:
        road_summaries.append(z)
    elif '_Roughness_' in z and '.csv' in z:
        roughness.append(z)
    elif '_Path_' in z and '.csv' in z:
        paths.append(z)
    elif 'Intervals_' in z and '.csv' in z:
        intervals.append(z)
    elif 'RoadPath_' in z and '.csv' in z:
        roadpaths.append(z)

print('total number of csvs with "RoadSummary" in the name: %d' % len(road_summaries))
print('total number of csvs with "_Path_" in the name: %d' % len(paths))
print('total number of csvs with "_Roughness_" in the name: %d' % len(roughness))
print('total number of csvs with "_Intervals_" in the name: %d' % len(intervals))
print('total number of csvs with "_RoadPath_" in the name: %d' % len(roadpaths))

# +
# New Style Traces
new_style_RLP_roots = []
for fil in roughness:
    root = os.path.join(*fil.split('\\')[:-1])
    subs = glob2.glob(root.replace('C:Users',r'C:\Users')+r'\**')
    counter = 0
    for sub in subs:
        if '_Roughness_' in sub and '.csv' in sub:
            counter = 1
    if counter == 1:
        new_style_RLP_roots.append(root)

valid_new_traces = []
for new in new_style_RLP_roots:
    subs = glob2.glob(new.replace('C:Users',r'C:\Users')+r'\**')
    intervals_counter, roadpaths_counter = 0, 0
    for sub in subs:
        if '_Roughness_' in sub and '.csv' in sub:
            interval_fil = sub
            intervals_counter = 1
        elif '_Path_' in sub and '.csv' in sub:
            roadpath_fil = sub
            roadpaths_counter = 1
    if intervals_counter == 1 and roadpaths_counter == 1:
        valid_new_traces.append([roadpath_fil, interval_fil])

# Old Style Traces
old_style_RLP_roots = []
for fil in intervals:
    root = os.path.join(*fil.split('\\')[:-1])
    subs = glob2.glob(root.replace('C:Users',r'C:\Users')+r'\**')
    counter = 0
    for sub in subs:
        if 'Intervals' in sub and '.csv' in sub:
            counter = 1
    if counter == 1:
        old_style_RLP_roots.append(root)

valid_old_traces = []
for old in old_style_RLP_roots:
    subs = glob2.glob(old.replace('C:Users',r'C:\Users')+r'\**')
    intervals_counter, roadpaths_counter = 0, 0
    for sub in subs:
        if 'Intervals' in sub and '.csv' in sub:
            interval_fil = sub
            intervals_counter = 1
        elif 'RoadPath' in sub and '.csv' in sub:
            roadpath_fil = sub
            roadpaths_counter = 1
    if intervals_counter == 1 and roadpaths_counter == 1:
        valid_old_traces.append([roadpath_fil, interval_fil])

print('Valid New-Style Data: %s' % len(valid_new_traces))
print('Valid Old-Style Data: %s' % len(valid_old_traces))
print('Total valid traces: %d' % (len(valid_old_traces) + len(valid_new_traces)))


# -

# ### Processing

class NewTrace(object):
    
    def __init__(self, path_list):
        self.path = path_list[0]
        self.roughness = path_list[1]
        
    def shittyPandasOpen(self, fileName):
            '''https://stackoverflow.com/questions/1365797/python-long-filename-support-broken-in-windows
            '''
            allResults = []
            lineCnt = 0
            fileName = r"\\?\%s" % fileName
            with open(fileName, 'r', encoding="utf8") as inFile:
                for line in inFile:
                    if lineCnt == 0:
                        titles = line.split('","')
                        titles[-1] = titles[-1].replace('\n','')
                        for t in range(0, len(titles)):
                            titles[t] = titles[t].replace('"','')
                    else:
                        data = line.split('","')
                        data[-1] = data[-1].replace(',\n','')
                        for t in range(0, len(data)):
                            data[t] = data[t].replace('"','')
                        allResults.append(data)
                    lineCnt = lineCnt + 1

            if len(allResults) > 0:
                xx = pd.DataFrame(allResults, columns=titles)
            else:  
                raise ValueError('Error: file has no length')

            return(xx)
        

    def load(self):
        
        ### Path file
        self.path_df = self.shittyPandasOpen(self.path) 
        self.path_df.columns = [x.upper() for x in self.path_df.columns]
        if self.path_df.columns[3] == 'POINT_LATIDUDE':
            inColumns = list(self.path_df.columns)
            inColumns[3] = 'POINT_LATITUDE'
            self.path_df.columns = inColumns
        self.path_df['Points'] = list(zip(self.path_df.POINT_LONGITUDE.astype(float), 
                                          self.path_df.POINT_LATITUDE.astype(float)))
        self.path_df['Points'] = self.path_df['Points'].apply(Point)
        if len(self.path_df) < 2:
            raise ValueError('Error: Path DataFrame contains one or fewer entries - cannot generate a line!')
            self.path_geometry = None
        else:
            self.path_geometry = LineString(list(self.path_df.Points))
        
        ### Roughness file
        self.roughness_df = self.shittyPandasOpen(self.roughness) 
        self.roughness_df.columns = [x.upper() for x in self.roughness_df.columns]
        try:
            if self.path_df.ROAD_IDENTIFICATION.iloc[0] == self.roughness_df.ROAD_IDENTIFICATION.iloc[0]:
                self.road_ID = self.path_df.ROAD_IDENTIFICATION.iloc[0]
            else:
                raise ValueError('Error: Path and Roughness files return different IDs!!')
        except:
            raise ValueError('Error: no "ROAD_IDENTIFICATION" field in DF - file corrupted')

    def checkGeometry(self, 
                      source = 'epsg:4326', 
                      target = 'epsg:3405', 
                      thresh = 300, 
                      min_coords = 4,
                      min_length = 200):
        
        if self.path_geometry == None:
            raise ValueError('Error: Cannot check geometry - No geometry to check!')
        else:
        
            self.new_geometry = self.path_geometry

            broken_roads = []
            breaks = []

            project_WGS_UTM = partial(
                    pyproj.transform,
                    pyproj.Proj(init=source),
                    pyproj.Proj(init=target))

            project_UTM_WGS = partial(
                        pyproj.transform,
                        pyproj.Proj(init=target),
                        pyproj.Proj(init=source))

            coord_list = self.path_geometry.coords

            for i in range(1, (len(coord_list))):
                st_coord = coord_list[i-1]
                end_coord = coord_list[i]
                segment = LineString([Point(st_coord),Point(end_coord)])
                proj_seg = transform(project_WGS_UTM, segment)

                if proj_seg.length > thresh:
                    breaks.append(i)

            if len(breaks) == 0:
                pass

            else:
                new_geoms = []
                breaks.append(len(coord_list))
                st = 0
                for i in breaks:
                    coord_seq = coord_list[st:i]
                    if len(coord_seq) > min_coords: 
                        new_feature = LineString(coord_seq)
                        new_feature_UTM = transform(project_WGS_UTM, new_feature)
                        if new_feature_UTM.length > min_length:
                            new_geoms.append(LineString(coord_seq))
                    st = i

                if len(new_geoms) == 1:
                    self.new_geometry = new_geoms[0]

                elif len(new_geoms) > 1:
                    self.new_geometry = MultiLineString(new_geoms)

                else:
                    print('invalid geometry! (%s) geoms' % len(new_geoms),self.road_ID,self.path)
                    self.new_geometry = None
    
    def conditionSummary(self,mapper):
        
        self.suspension = self.roughness_df.SUSPENSION_TYPE.iloc[0]
        
        cut = self.roughness_df.copy()
        cut.CONDITION_CATEGORY = cut.CONDITION_CATEGORY.apply(lambda x: x.upper())
        cut = cut.loc[cut.CONDITION_CATEGORY != 'NOT MEASURED']
        cut.CONDITION_CATEGORY = cut.CONDITION_CATEGORY.map(mapper)
        cut.INTERVAL_LENGTH = cut.INTERVAL_LENGTH.astype(float)
        total_interval_length = cut.INTERVAL_LENGTH.sum()
        cut['WEIGHTED'] = cut.INTERVAL_LENGTH * cut.CONDITION_CATEGORY
        average_state = cut.WEIGHTED.sum() / total_interval_length
        self.condition = (round(average_state))
        
        cut = self.roughness_df.copy()
        cut = cut.loc[cut.ROUGHNESS != '']
        cut.INTERVAL_LENGTH = cut.INTERVAL_LENGTH.astype(float)
        cut.ROUGHNESS = cut.ROUGHNESS.astype(float)
        cut['WEIGHTED_IRI'] = cut.INTERVAL_LENGTH * cut.ROUGHNESS
        average_state = cut.WEIGHTED_IRI.sum() / total_interval_length
        self.IRI_average = average_state


class OldTrace(object):
    
    def __init__(self, path_list):
        self.path = path_list[0]
        self.roughness = path_list[1]
        
    def shittyPandasOpen(self, fileName):
        '''https://stackoverflow.com/questions/1365797/python-long-filename-support-broken-in-windows
        '''
        allResults = []
        lineCnt = 0
        fileName = r"\\?\%s" % fileName
        with open(fileName, 'r', encoding="utf8") as inFile:
            for line in inFile:
                line = line.replace('"', '')
                if lineCnt == 0:
                    titles = line.split(",")
                    titles[-1] = titles[-1].replace('\n','')
                else:
                    data_line = line.split(",")
                    data_line[-1] = data_line[-1].replace('\n','')
                    allResults.append(data_line)
                lineCnt = lineCnt + 1
        
        if len(allResults) > 0:
            xx = pd.DataFrame(allResults, columns=titles[:len(allResults[0])])
        else:  
            raise ValueError('Error: file has no length')
            
        return(xx)

    def load(self):
        
        ### Path file
        self.path_df = self.shittyPandasOpen(self.path) 
        self.path_df.columns = [x.upper() for x in self.path_df.columns]
        self.path_df['Points'] = list(zip(self.path_df.LONGITUDE.astype(float), 
                                          self.path_df.LATITUDE.astype(float)))
        self.path_df['Points'] = self.path_df['Points'].apply(Point)
        if len(self.path_df) < 2:
            raise ValueError('Error: Path DataFrame contains one or fewer entries - cannot generate a line!')
            self.path_geometry = None
        else:
            self.path_geometry = LineString(list(self.path_df.Points))
        
        ### Roughness file
        self.roughness_df = self.shittyPandasOpen(self.roughness) 
        self.roughness_df.columns = [x.upper() for x in self.roughness_df.columns]
    
    def checkGeometry(self, 
                      source = 'epsg:4326', 
                      target = 'epsg:3405', 
                      thresh = 300, 
                      min_coords = 4, 
                      min_length = 200):
        
        if self.path_geometry == None:
            raise ValueError('Error: Cannot check geometry - No geometry to check!')
        else:
            self.new_geometry = self.path_geometry
        
            broken_roads = []
            breaks = []

            project_WGS_UTM = partial(
                    pyproj.transform,
                    pyproj.Proj(init=source),
                    pyproj.Proj(init=target))

            project_UTM_WGS = partial(
                        pyproj.transform,
                        pyproj.Proj(init=target),
                        pyproj.Proj(init=source))

            coord_list = self.path_geometry.coords

            for i in range(1, (len(coord_list))):
                st_coord = coord_list[i-1]
                end_coord = coord_list[i]
                segment = LineString([Point(st_coord),Point(end_coord)])
                proj_seg = transform(project_WGS_UTM, segment)

                if proj_seg.length > thresh:
                    breaks.append(i)

            if len(breaks) == 0:
                pass

            else:
                new_geoms = []
                breaks.append(len(coord_list))
                st = 0
                for i in breaks:
                    coord_seq = coord_list[st:i]
                    if len(coord_seq) > min_coords: 
                        new_feature = LineString(coord_seq)
                        new_feature_UTM = transform(project_WGS_UTM, new_feature)
                        if new_feature_UTM.length > min_length:
                            new_geoms.append(LineString(coord_seq))
                    st = i

                if len(new_geoms) == 1:
                    self.new_geometry = new_geoms[0]

                elif len(new_geoms) > 1:
                    self.new_geometry = MultiLineString(new_geoms)

                else:
                    print('invalid geometry! (%s) geoms' % len(new_geoms),self.road_ID,self.path)
                    self.new_geometry = None
    
    def conditionSummary(self,mapper):
                
        self.roughness_df.CATEGORY = self.roughness_df.CATEGORY.map(mapper)
        self.roughness_df.DISTANCE = self.roughness_df.DISTANCE.astype(float)
        total_interval_length = self.roughness_df.DISTANCE.sum()
        self.roughness_df['WEIGHTED_CAT'] = self.roughness_df.DISTANCE * self.roughness_df.CATEGORY
        average_state = self.roughness_df.WEIGHTED_CAT.sum() / total_interval_length
        self.condition = (round(average_state))
        
        self.roughness_df.IRI = self.roughness_df.IRI.astype(float)
        self.roughness_df['WEIGHTED_IRI'] = self.roughness_df.DISTANCE * self.roughness_df.IRI
        average_state = self.roughness_df.WEIGHTED_IRI.sum() / total_interval_length
        self.IRI_average = (average_state)
        self.suspension = tr.roughness_df.SUSPENSION.iloc[0]
    
    def searchPathforVPROMMS(self):
        search_res = re.search(r"[1-9][1-3][2-4].*\\", self.path)
        if search_res == None:
            base_pth = r'C:\\Users\\charl\\Documents\\GOST\\Vietnam\\unzipped'
            self.road_ID = os.path.relpath(self.path, base_pth) 
        else:
            self.road_ID = search_res.group().split('\\')[0]


# +
# %%time

mapper = {'NOT MEASURED':None,
         'VERY POOR':0,
         'POOR':1,
         'FAIR':2.0,
         'GOOD':3.0,
         'VERY GOOD':4.0,
         'EXCELLENT':5.0}

error_paths = []
base_pth = r'C:\\Users\\charl\\Documents\\GOST\\Vietnam\\unzipped'

error_counter = 0
processed_new_traces = []
val = 0
verbose = 0
thresh = 300 # metres beyond which to declare points erroneous
min_coords = 3 # minimum coordinate number for a new geometry object
min_length = 150 # minimum length, in metres, for a new geometry object

print('New model: Commencing trace processing')
for trace_candidate in valid_new_traces:
    try:
        trace = NewTrace(trace_candidate)
        trace.load()
        trace.checkGeometry(thresh = thresh, min_coords = 4, min_length = 200)
        trace.conditionSummary(mapper)
        processed_new_traces.append(trace)
    except ValueError as err:
        if verbose == 1:
            print("Value Error: %s, %s, %s" % (err, val, trace.path))
        error_counter+=1
        error_paths.append([os.path.relpath(trace_candidate[0], base_pth),err])
    except AttributeError as err:
        if verbose == 1:
            print("Attribute Error: %s, %s, %s" % (err, val, trace.path))
        error_counter+=1
        error_paths.append([os.path.relpath(trace_candidate[0], base_pth),err])
    except AssertionError as err:
        if verbose == 1:
            print("Weird column error ... probably: %s, %s, %s" % (err, val, trace.path))
        error_counter+=1
        error_paths.append([os.path.relpath(trace_candidate[0], base_pth),err])
    val+=1
        
print('\nNew model: traces generated: %s' % len(new_traces))
print('New model: errors encountered: %s' % error_counter)

error_counter = 0
processed_old_traces = []
print('\nOld model: Commencing trace processing')
for trace_candidate in valid_old_traces:
    try:
        trace = OldTrace(trace_candidate)
        trace.load()
        trace.checkGeometry(thresh = thresh, min_coords = 4, min_length = 200)
        trace.searchPathforVPROMMS()
        trace.conditionSummary(mapper)
        processed_old_traces.append(trace)
    except:
        error_counter+=1
        error_paths.append([os.path.relpath(trace_candidate[0], base_pth),'unknown'])
        pass

print('\nOld model: traces generated: %s' % len(old_traces))
print('Old model: errors encountered: %s' % error_counter)

processing_errors = pd.DataFrame(error_paths, columns = ['FilePath','Error'])
processing_errors['road_ID'] = 'trace never formed - cannot determine road_ID'
# -

# ### Form DataFrame of Processed Traces

rows = []
for trace in new_traces:
    rows.append({
        'province':os.path.relpath(trace.path, base_pth).split('\\')[0],
        'road_ID':trace.road_ID,
        'geometry':trace.new_geometry,
        'condition':trace.condition,
        'IRI_average':trace.IRI_average, 
        'suspension':trace.suspension,
        'trace_type':'new_trace',
        'FilePath':os.path.relpath(trace.path, base_pth)
    })
for trace in old_traces:
    rows.append({
        'province':os.path.relpath(trace.path, base_pth).split('\\')[0],
        'road_ID':trace.road_ID,
        'geometry':trace.new_geometry,
        'condition':trace.condition,
        'IRI_average':trace.IRI_average, 
        'suspension':trace.suspension,
        'trace_type':'old_trace',
        'FilePath':os.path.relpath(trace.path, base_pth)
    })
df = pd.DataFrame(rows)
df['geom_class'] = df['geometry'].apply(type).astype(str)
df['geom_class'] = df['geom_class'].map({r"<class 'shapely.geometry.linestring.LineString'>":'Line',
                     r"<class 'shapely.geometry.multilinestring.MultiLineString'>":'MultiLine',
                    r"<class 'NoneType'>":'NoneType'})
df = df[['province','road_ID','trace_type','IRI_average','condition','suspension','geometry','geom_class','FilePath']]

# ### Move Errors to Error File

null_geom_entires = df[['FilePath','road_ID']].loc[df.geom_class == 'NoneType']
null_geom_entires['Error'] = 'Error: Invalid geometry - no points within %s meters of eachother' % thresh

errors = pd.concat([processing_errors,null_geom_entires], axis = 0)

# ### Edit Road IDs for duplicates - search for link information or supply unique indexer

# +
df = df.loc[df.geom_class != 'NoneType']
df2 = df.groupby('road_ID')
df['indexer'] = df2.cumcount()

duplicate_ids_dfs = []
for i in list(df2):
    if len(i[1]) == 1:
        pass
    else:
         duplicate_ids_dfs.append(i)

def check(x):
    base = x.road_ID
    search_term = x.FilePath
    search_res = re.search(r"Link.*\\", search_term)
    if search_res == None:
        return base + '_' + str(x.indexer)
    else:
        link_info = search_res.group().split('_')[0:2]
        link_info = "_".join(link_info)
        return base + '_' + link_info + str(x.indexer)
    
old, new = [], []
for i in range(0, len(duplicate_ids_dfs)):
    old.append(list(df.loc[duplicate_ids_dfs[i][1].index, 'road_ID']))
    df.loc[duplicate_ids_dfs[i][1].index, 'road_ID'] = df.loc[duplicate_ids_dfs[i][1].index].apply(lambda x: check(x), axis = 1)
    new.append(list(df.loc[duplicate_ids_dfs[i][1].index, 'road_ID']))
old = [item for sublist in old for item in sublist]
new = [item for sublist in new for item in sublist]
translations = dict(zip(old, new))
# -

# ### Save Down

# +
df = df.drop(['indexer'], axis = 1)
gdf = gpd.GeoDataFrame(df, crs = {'init':'epsg:4326'}, geometry = 'geometry') 
gdf.to_file(os.path.join(r'C:\Users\charl\Documents\GOST\Vietnam\outter','roads.shp'),driver = 'ESRI Shapefile', encoding = 'utf-8')

gdf = gpd.GeoDataFrame(df, crs = {'init':'epsg:4326'}, geometry = 'geometry') 
gdf = gdf.to_crs({'init':'epsg:3405'})
gdf['length(km)'] = gdf.length / 1000
summary = gdf[['province','length(km)']].groupby('province').sum().sort_values(by = 'length(km)', ascending = False)
summary_count = gdf[['province','length(km)']].groupby('province').count()
summary['count'] = summary_count['length(km)']
summary.to_csv(os.path.join(r'C:\Users\charl\Documents\GOST\Vietnam\outter','summary_stats.csv'))

errors.to_csv(os.path.join(r'C:\Users\charl\Documents\GOST\Vietnam\outter','errors.csv'))
# -

df.geom_class.value_counts()



StarcoderdataJupyterScriptsDedupFiltered