From 4cd819713280c80d68e6aadee37c9b1fb10b8c89 Mon Sep 17 00:00:00 2001 From: Jan-Lukas Else Date: Sat, 24 Jun 2023 16:12:50 +0200 Subject: [PATCH] Initial commit --- LICENSE | 21 +++++++ README.md | 76 +++++++++++++++++++++++ filter/main.go | 126 ++++++++++++++++++++++++++++++++++++++ go.mod | 3 + web/main.go | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 387 insertions(+) create mode 100644 LICENSE create mode 100644 README.md create mode 100644 filter/main.go create mode 100644 go.mod create mode 100644 web/main.go diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..880bfa8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 Jan-Lukas Else + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..308f9bf --- /dev/null +++ b/README.md @@ -0,0 +1,76 @@ +# Cell Location Map using Mozilla Location Service + +This repository provides code to display the cell locations using the Mozilla Location Service. The code extracts cell data from a CSV export file and generates an interactive map with markers representing the cell locations. + +## Prerequisites + +- Go programming language +- Access to a [cell export file from the Mozilla Location Service](https://location.services.mozilla.com/downloads) + +## Installation + +1. Clone the repository: + + ```shell + git clone https://git.jlel.se/jlelse/mls.git + ``` + +2. Change to the project directory: + + ```shell + cd mls + ``` + +## Usage + +### Step 1: Filter Cell Data + +To filter the cell data and generate an output file, run the following command: + +```shell +go run ./filter/main.go +``` + +- ``: Path to the input CSV file containing the cell data. +- ``: Path to the output CSV file where the filtered data will be saved. +- ``: The desired radio type (e.g., LTE). +- ``: The desired Mobile Country Code (e.g., 262 for Germany). +- ``: The desired network code (e.g., 3 for o2-de). +- ``: The minimum number of samples required for a cell to be included. +- ``: Space-separated list of columns to include in the output. + +Example: + +```shell +go run ./filter/main.go ./MLS-full-cell-export.csv ./output.csv LTE 262 3 100 lon lat created updated cell +``` + +### Step 2: Generate Map + +To generate an HTML map with the filtered cell data, run the following command: + +```shell +go run ./web/main.go +``` + +- ``: Path to the input CSV file containing the filtered cell data with the columns lon,lat,created,updated,cell. +- ``: Path to the output HTML file where the map will be saved. + +Example: + +```shell +go run ./web/main.go ./output.csv ./map.html +``` + +## Acknowledgments + +The code in this repository is based on the [Mozilla Location Service](https://location.services.mozilla.com/) and leverages the following open-source libraries: + +- [Leaflet](https://leafletjs.com/) (version 1.9.4): An open-source JavaScript library for interactive maps. +- [Leaflet.markercluster](https://github.com/Leaflet/Leaflet.markercluster) (version 1.4.1): A plugin for Leaflet that provides clustering functionality for markers. + +The idea for this project was inspired by the [CellInspector Map](https://mls.maxomagier.de/map.html). + +## License + +This project is licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/filter/main.go b/filter/main.go new file mode 100644 index 0000000..084cde2 --- /dev/null +++ b/filter/main.go @@ -0,0 +1,126 @@ +package main + +import ( + "encoding/csv" + "fmt" + "io" + "os" +) + +func main() { + // Check if the correct number of command-line arguments is provided + if len(os.Args) < 6 { + fmt.Println("Usage: go run main.go ") + return + } + + // Get the command-line arguments + inputFile := os.Args[1] + outputFile := os.Args[2] + desiredRadio := os.Args[3] + desiredMCC := os.Args[4] + desiredNetwork := os.Args[5] + desiredMinSamples := os.Args[6] + desiredColumns := os.Args[7:] + + // Open the input CSV file + input, err := os.Open(inputFile) + if err != nil { + fmt.Println("Error opening the input file:", err) + return + } + defer input.Close() + + // Create the output CSV file + output, err := os.Create(outputFile) + if err != nil { + fmt.Println("Error creating the output file:", err) + return + } + defer output.Close() + + // CSV reader and writer initialization + reader := csv.NewReader(input) + writer := csv.NewWriter(output) + + // Read the header row to get the column indices + header, err := reader.Read() + if err != nil { + fmt.Println("Error reading CSV header:", err) + return + } + + radioIndex := findColumnIndex(header, "radio") + mccIndex := findColumnIndex(header, "mcc") + netIndex := findColumnIndex(header, "net") + samplesIndex := findColumnIndex(header, "samples") + + // Find the indices of the desired columns + desiredIndices := make([]int, len(desiredColumns)) + for i, column := range desiredColumns { + desiredIndices[i] = findColumnIndex(header, column) + if desiredIndices[i] == -1 { + fmt.Println("Column not found:", column) + return + } + } + + // Write the header row to the output file + err = writer.Write(desiredColumns) + if err != nil { + fmt.Println("Error writing the header row:", err) + return + } + + // Read the CSV data line by line + for { + row, err := reader.Read() + if err == io.EOF { + break + } else if err != nil { + fmt.Println("Error reading CSV data:", err) + return + } + + // Check the filter criteria + radio := row[radioIndex] + mcc := row[mccIndex] + net := row[netIndex] + samples := row[samplesIndex] + + if radio == desiredRadio && mcc == desiredMCC && net == desiredNetwork && samples >= desiredMinSamples { + // Extract the desired columns based on indices + filteredRow := make([]string, len(desiredIndices)) + for i, index := range desiredIndices { + filteredRow[i] = row[index] + } + + // Write the row to the output file + err := writer.Write(filteredRow) + if err != nil { + fmt.Println("Error writing the row:", err) + return + } + } + } + + // Write CSV writer data to the file + writer.Flush() + + if err := writer.Error(); err != nil { + fmt.Println("Error writing the output file:", err) + return + } + + fmt.Println("Filtering completed.") +} + +// Helper function to find the index of a column name in the header row +func findColumnIndex(header []string, columnName string) int { + for i, name := range header { + if name == columnName { + return i + } + } + return -1 +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..628ec3f --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.jlel.se/jlelse/mls + +go 1.20 diff --git a/web/main.go b/web/main.go new file mode 100644 index 0000000..c21d2a3 --- /dev/null +++ b/web/main.go @@ -0,0 +1,161 @@ +package main + +import ( + "encoding/csv" + "fmt" + "log" + "os" + "strconv" + "strings" + "time" +) + +const ( + leafletVersion = "1.9.4" + markerClusterVersion = "1.4.1" + htmlTemplateStart = ` + + + Location Map + + + + + + +
+ + + + +` +) + +func main() { + if len(os.Args) != 3 { + fmt.Println("Usage: go run main.go ") + return + } + + inputFile := os.Args[1] + outputFile := os.Args[2] + + // Open the CSV file + file, err := os.Open(inputFile) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + // Read the CSV records + reader := csv.NewReader(file) + records, err := readCSVRecords(reader) + if err != nil { + log.Fatal(err) + } + + // Generate the HTML + html := generateHTML(records) + + // Save the HTML to a file + err = writeHTMLToFile(outputFile, html) + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Map generated successfully and saved to %s.\n", outputFile) +} + +func readCSVRecords(reader *csv.Reader) ([][]string, error) { + reader.Comma = ',' // Set the delimiter to comma explicitly + records, err := reader.ReadAll() + if err != nil { + return nil, err + } + + // Remove the header row + if len(records) > 0 { + records = records[1:] + } + + return records, nil +} + +func generateHTML(records [][]string) string { + var htmlBuilder strings.Builder + htmlBuilder.WriteString(fmt.Sprintf(htmlTemplateStart, leafletVersion, markerClusterVersion, markerClusterVersion, leafletVersion, markerClusterVersion)) + + for _, record := range records { + // Parse longitude and latitude + lon, err := strconv.ParseFloat(record[0], 64) + if err != nil { + log.Fatalf("Error parsing longitude: %v\nLongitude value: %s\n", err, record[0]) + } + lat, err := strconv.ParseFloat(record[1], 64) + if err != nil { + log.Fatalf("Error parsing latitude: %v\nLatitude value: %s\n", err, record[1]) + } + + // Get created and updated timestamps + created := record[2] + updated := record[3] + cell := record[4] + + htmlBuilder.WriteString(fmt.Sprintf(` + var marker = L.marker([%f, %f]); + marker.bindTooltip("Created: %s
Updated: %s
Cell: %s"); + markers.addLayer(marker); + `, lat, lon, formatTimestamp(created), formatTimestamp(updated), cell)) + } + + htmlBuilder.WriteString(htmlTemplateEnd) + return htmlBuilder.String() +} + +func formatTimestamp(timestamp string) string { + // Parse the Unix timestamp string to an integer + unixTime, err := strconv.ParseInt(timestamp, 10, 64) + if err != nil { + return "" // Return an empty string if parsing fails + } + + // Convert the Unix timestamp to a time.Time value + timeValue := time.Unix(unixTime, 0) + + // Format the time + isoFormatted := timeValue.Format(time.RFC3339) + + return isoFormatted +} + +func writeHTMLToFile(filename string, html string) error { + file, err := os.Create(filename) + if err != nil { + return err + } + defer file.Close() + + _, err = file.WriteString(html) + if err != nil { + return err + } + + return nil +}