Initial commit
This commit is contained in:
commit
4cd8197132
|
@ -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.
|
|
@ -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 <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>
|
||||
```
|
||||
|
||||
- `<input CSV file>`: Path to the input CSV file containing the cell data.
|
||||
- `<output CSV file>`: Path to the output CSV file where the filtered data will be saved.
|
||||
- `<radio>`: The desired radio type (e.g., LTE).
|
||||
- `<mcc>`: The desired Mobile Country Code (e.g., 262 for Germany).
|
||||
- `<network>`: The desired network code (e.g., 3 for o2-de).
|
||||
- `<min samples>`: The minimum number of samples required for a cell to be included.
|
||||
- `<output columns>`: 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 <input CSV file> <output HTML file>
|
||||
```
|
||||
|
||||
- `<input CSV file>`: Path to the input CSV file containing the filtered cell data with the columns lon,lat,created,updated,cell.
|
||||
- `<output HTML file>`: 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).
|
|
@ -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 <input CSV file> <output CSV file> <radio> <mcc> <network> <min samples> <output columns>")
|
||||
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
|
||||
}
|
|
@ -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 = `<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Location Map</title>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.css" />
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@%s/dist/MarkerCluster.Default.css" />
|
||||
<style>
|
||||
#map {
|
||||
height: 800px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="map"></div>
|
||||
<script src="https://cdn.jsdelivr.net/npm/leaflet@%s/dist/leaflet.js"></script>
|
||||
<script src="https://unpkg.com/leaflet.markercluster@%s/dist/leaflet.markercluster.js"></script>
|
||||
<script>
|
||||
var map = L.map('map').setView([0, 0], 2);
|
||||
|
||||
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||
attribution: 'Map data © <a href="https://www.openstreetmap.org/">OpenStreetMap</a> contributors',
|
||||
maxZoom: 18,
|
||||
}).addTo(map);
|
||||
|
||||
var markers = L.markerClusterGroup();
|
||||
|
||||
`
|
||||
htmlTemplateEnd = `
|
||||
map.addLayer(markers);
|
||||
</script>
|
||||
</body>
|
||||
</html>`
|
||||
)
|
||||
|
||||
func main() {
|
||||
if len(os.Args) != 3 {
|
||||
fmt.Println("Usage: go run main.go <input CSV file (needs lon,lat,created,updated,cell)> <output HTML file>")
|
||||
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<br />Updated: %s<br />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
|
||||
}
|
Loading…
Reference in New Issue