Add oramap
This commit is contained in:
parent
2334ec08a2
commit
c410843f5f
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
node_modules/
|
||||||
|
package-lock.json
|
||||||
|
package.json
|
||||||
|
|
||||||
10
Dockerfile
Normal file
10
Dockerfile
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
FROM node:24-slim
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY package*.json ./
|
||||||
|
RUN npm install
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
CMD ["node", "server.js"]
|
||||||
6
README copy.md
Normal file
6
README copy.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
# How to Run
|
||||||
|
```
|
||||||
|
npm init -y
|
||||||
|
npm install express
|
||||||
|
node server.js
|
||||||
|
```
|
||||||
10
data/families.json
Normal file
10
data/families.json
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
[
|
||||||
|
{ "family": "Kafe (קאפח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Shiheb (שחב-שבח)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Manakhah (מנאכה)", "lat": 15.3019, "lng": 43.5983 },
|
||||||
|
{ "family": "Uzeyri (עזירי-עוזרי)", "city": "Dhamar (ד'מאר)", "lat": 14.5424, "lng": 44.4056 },
|
||||||
|
{ "family": "Salumi (סלומי-שלומי)", "city": "Al Kafla (אל קפלה)", "lat": 16.0240, "lng": 43.9790 },
|
||||||
|
{ "family": "Afgin (עפג'ין)", "city": "Sa'dah (צעדה)", "lat": 16.9402, "lng": 43.7639 },
|
||||||
|
{ "family": "Eraki (עראקי)", "city": "Sana'a (צנעא)", "lat": 15.3545, "lng": 44.2064 }
|
||||||
|
]
|
||||||
23
oramap/.helmignore
Normal file
23
oramap/.helmignore
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Patterns to ignore when building packages.
|
||||||
|
# This supports shell glob matching, relative path matching, and
|
||||||
|
# negation (prefixed with !). Only one pattern per line.
|
||||||
|
.DS_Store
|
||||||
|
# Common VCS dirs
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.bzr/
|
||||||
|
.bzrignore
|
||||||
|
.hg/
|
||||||
|
.hgignore
|
||||||
|
.svn/
|
||||||
|
# Common backup files
|
||||||
|
*.swp
|
||||||
|
*.bak
|
||||||
|
*.tmp
|
||||||
|
*.orig
|
||||||
|
*~
|
||||||
|
# Various IDEs
|
||||||
|
.project
|
||||||
|
.idea/
|
||||||
|
*.tmproj
|
||||||
|
.vscode/
|
||||||
24
oramap/Chart.yaml
Normal file
24
oramap/Chart.yaml
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: v2
|
||||||
|
name: oramap
|
||||||
|
description: A Helm chart for Kubernetes
|
||||||
|
|
||||||
|
# A chart can be either an 'application' or a 'library' chart.
|
||||||
|
#
|
||||||
|
# Application charts are a collection of templates that can be packaged into versioned archives
|
||||||
|
# to be deployed.
|
||||||
|
#
|
||||||
|
# Library charts provide useful utilities or functions for the chart developer. They're included as
|
||||||
|
# a dependency of application charts to inject those utilities and functions into the rendering
|
||||||
|
# pipeline. Library charts do not define any templates and therefore cannot be deployed.
|
||||||
|
type: application
|
||||||
|
|
||||||
|
# This is the chart version. This version number should be incremented each time you make changes
|
||||||
|
# to the chart and its templates, including the app version.
|
||||||
|
# Versions are expected to follow Semantic Versioning (https://semver.org/)
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
# This is the version number of the application being deployed. This version number should be
|
||||||
|
# incremented each time you make changes to the application. Versions are not expected to
|
||||||
|
# follow Semantic Versioning. They should reflect the version the application is using.
|
||||||
|
# It is recommended to use it with quotes.
|
||||||
|
appVersion: "1.16.0"
|
||||||
11
oramap/templates/_helpers.tpl
Normal file
11
oramap/templates/_helpers.tpl
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{{- define "oramap.name" -}}
|
||||||
|
{{ .Chart.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "oramap.fullname" -}}
|
||||||
|
{{ include "oramap.name" . }}-{{ .Release.Name }}
|
||||||
|
{{- end }}
|
||||||
|
|
||||||
|
{{- define "oramap.chart" -}}
|
||||||
|
{{ .Chart.Name }}-{{ .Chart.Version | replace "+" "_" }}
|
||||||
|
{{- end }}
|
||||||
38
oramap/templates/deployment.yaml
Normal file
38
oramap/templates/deployment.yaml
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: {{ include "oramap.fullname" . }}
|
||||||
|
labels:
|
||||||
|
app: {{ include "oramap.name" . }}
|
||||||
|
chart: {{ include "oramap.chart" . }}
|
||||||
|
release: {{ .Release.Name }}
|
||||||
|
heritage: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
replicas: {{ .Values.replicaCount }}
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app: {{ include "oramap.name" . }}
|
||||||
|
release: {{ .Release.Name }}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app: {{ include "oramap.name" . }}
|
||||||
|
release: {{ .Release.Name }}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: {{ .Chart.Name }}
|
||||||
|
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
|
||||||
|
imagePullPolicy: {{ .Values.image.pullPolicy }}
|
||||||
|
ports:
|
||||||
|
- containerPort: {{ .Values.containerPort }}
|
||||||
|
name: http
|
||||||
|
livenessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
readinessProbe:
|
||||||
|
httpGet:
|
||||||
|
path: /
|
||||||
|
port: http
|
||||||
|
resources:
|
||||||
|
{{- toYaml .Values.resources | nindent 12 }}
|
||||||
33
oramap/templates/ingress.yaml
Normal file
33
oramap/templates/ingress.yaml
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
{{- if .Values.ingress.enabled }}
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: {{ include "oramap.fullname" . }}
|
||||||
|
annotations:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
kubernetes.io/ingress.class: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
spec:
|
||||||
|
{{- if .Values.ingress.className }}
|
||||||
|
ingressClassName: {{ .Values.ingress.className }}
|
||||||
|
{{- end }}
|
||||||
|
rules:
|
||||||
|
{{- range .Values.ingress.hosts }}
|
||||||
|
- host: {{ .host }}
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
{{- range .paths }}
|
||||||
|
- path: {{ .path }}
|
||||||
|
pathType: {{ .pathType }}
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: {{ include "oramap.fullname" $ }}
|
||||||
|
port:
|
||||||
|
number: {{ $.Values.service.port }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
|
{{- if .Values.ingress.tls }}
|
||||||
|
tls:
|
||||||
|
{{- toYaml .Values.ingress.tls | nindent 4 }}
|
||||||
|
{{- end }}
|
||||||
|
{{- end }}
|
||||||
19
oramap/templates/service.yaml
Normal file
19
oramap/templates/service.yaml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: {{ include "oramap.fullname" . }}
|
||||||
|
labels:
|
||||||
|
app: {{ include "oramap.name" . }}
|
||||||
|
chart: {{ include "oramap.chart" . }}
|
||||||
|
release: {{ .Release.Name }}
|
||||||
|
heritage: {{ .Release.Service }}
|
||||||
|
spec:
|
||||||
|
type: {{ .Values.service.type }}
|
||||||
|
ports:
|
||||||
|
- port: {{ .Values.service.port }}
|
||||||
|
targetPort: {{ .Values.containerPort }}
|
||||||
|
protocol: TCP
|
||||||
|
name: http
|
||||||
|
selector:
|
||||||
|
app: {{ include "oramap.name" . }}
|
||||||
|
release: {{ .Release.Name }}
|
||||||
22
oramap/values.yaml
Normal file
22
oramap/values.yaml
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
replicaCount: 1
|
||||||
|
|
||||||
|
image:
|
||||||
|
repository: harbor.dvirlabs.com/shay/oramap
|
||||||
|
tag: "1"
|
||||||
|
pullPolicy: IfNotPresent
|
||||||
|
|
||||||
|
containerPort: 3000
|
||||||
|
|
||||||
|
service:
|
||||||
|
type: ClusterIP
|
||||||
|
port: 80
|
||||||
|
|
||||||
|
ingress:
|
||||||
|
enabled: true
|
||||||
|
className: "traefik"
|
||||||
|
hosts:
|
||||||
|
- host: oramap.dvirlabs.com
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
tls: []
|
||||||
26
public/index.html
Normal file
26
public/index.html
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<title>🗺️ Ora</title>
|
||||||
|
<link rel="stylesheet" href="style.css">
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet/dist/leaflet.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1>🗺️ Ora</h1>
|
||||||
|
<div class="search-bar">
|
||||||
|
<input type="text" id="searchInput" placeholder="Enter family name..." />
|
||||||
|
<button onclick="searchFamily()">Search</button>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<div id="map"></div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet/dist/leaflet.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/fuse.js/dist/fuse.min.js"></script>
|
||||||
|
<script src="script.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
BIN
public/logo-shay.png
Normal file
BIN
public/logo-shay.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 975 KiB |
BIN
public/logo.png
Normal file
BIN
public/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 18 KiB |
124
public/script.js
Normal file
124
public/script.js
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
let map = L.map('map').setView([15.5527, 48.5164], 6);
|
||||||
|
|
||||||
|
// Define the two tile layers
|
||||||
|
const voyager = L.tileLayer('https://{s}.basemaps.cartocdn.com/rastertiles/voyager/{z}/{x}/{y}{r}.png', {
|
||||||
|
attribution: '© OpenStreetMap contributors © CARTO',
|
||||||
|
subdomains: 'abcd',
|
||||||
|
maxZoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
const openStreetMap = L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
|
||||||
|
attribution: 'Map data © OpenStreetMap contributors',
|
||||||
|
maxZoom: 19
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add the default layer (Voyager)
|
||||||
|
voyager.addTo(map);
|
||||||
|
|
||||||
|
// Group the layers for switching
|
||||||
|
const baseMaps = {
|
||||||
|
"English Map (CartoDB Voyager)": voyager,
|
||||||
|
"Original Map (OpenStreetMap)": openStreetMap
|
||||||
|
};
|
||||||
|
|
||||||
|
// Add the layer control to the map
|
||||||
|
L.control.layers(baseMaps).addTo(map);
|
||||||
|
|
||||||
|
async function searchFamily() {
|
||||||
|
const familyName = document.getElementById('searchInput').value.trim();
|
||||||
|
if (!familyName) {
|
||||||
|
alert('Please enter a family name.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/search?family=${encodeURIComponent(familyName)}`);
|
||||||
|
const familyResult = await response.json();
|
||||||
|
|
||||||
|
if (!familyResult.length) {
|
||||||
|
alert('No matching family found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearMarkers();
|
||||||
|
|
||||||
|
familyResult.forEach(record => {
|
||||||
|
L.marker([record.lat, record.lng]).addTo(map)
|
||||||
|
.bindPopup(`<strong>${record.family}</strong><br>City: ${record.city}`)
|
||||||
|
.openPopup();
|
||||||
|
});
|
||||||
|
|
||||||
|
const first = familyResult[0];
|
||||||
|
map.setView([first.lat, first.lng], 7);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Search error:', error);
|
||||||
|
alert('Something went wrong while searching. Please try again later.');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearMarkers() {
|
||||||
|
map.eachLayer(layer => {
|
||||||
|
if (layer instanceof L.Marker) {
|
||||||
|
map.removeLayer(layer);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const familyNames = [
|
||||||
|
"Kafe (קאפח)", "Shiheb (שחב-שבח)", "Eraki (עראקי)", "Salumi (סלומי-שלומי)",
|
||||||
|
"Afgin (עפג'ין)", "Uzeyri (עזירי-עוזרי)"
|
||||||
|
// Add more families here if you like
|
||||||
|
];
|
||||||
|
|
||||||
|
// Initialize Fuse.js
|
||||||
|
const fuse = new Fuse(familyNames, {
|
||||||
|
includeScore: true,
|
||||||
|
threshold: 0.4 // Lower = stricter matching
|
||||||
|
});
|
||||||
|
|
||||||
|
const searchInput = document.getElementById('searchInput');
|
||||||
|
const suggestionsBox = document.createElement('div');
|
||||||
|
suggestionsBox.classList.add('suggestions');
|
||||||
|
searchInput.parentNode.style.position = 'relative'; // Make parent relative
|
||||||
|
searchInput.parentNode.appendChild(suggestionsBox);
|
||||||
|
|
||||||
|
searchInput.addEventListener('input', () => {
|
||||||
|
const value = searchInput.value.trim();
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
|
||||||
|
if (!value) return;
|
||||||
|
|
||||||
|
const results = fuse.search(value);
|
||||||
|
|
||||||
|
results.forEach(result => {
|
||||||
|
const option = document.createElement('div');
|
||||||
|
option.textContent = result.item;
|
||||||
|
option.onclick = () => {
|
||||||
|
searchInput.value = result.item;
|
||||||
|
suggestionsBox.innerHTML = '';
|
||||||
|
};
|
||||||
|
suggestionsBox.appendChild(option);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.logo = function (opts) {
|
||||||
|
return new L.Control.Logo(opts);
|
||||||
|
};
|
||||||
|
|
||||||
|
L.Control.Logo = L.Control.extend({
|
||||||
|
onAdd: function () {
|
||||||
|
const div = L.DomUtil.create('div', 'custom-logo');
|
||||||
|
div.innerHTML = `
|
||||||
|
<img src="logo.png" alt="Logo" style="height: 40px; vertical-align: middle;">
|
||||||
|
<span style="margin-left: 8px; font-weight: bold; color: #333;">Shevach</span>
|
||||||
|
`;
|
||||||
|
return div;
|
||||||
|
},
|
||||||
|
|
||||||
|
onRemove: function () {
|
||||||
|
// Nothing to clean up
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
L.control.logo({ position: 'bottomleft' }).addTo(map);
|
||||||
98
public/style.css
Normal file
98
public/style.css
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
/* Reset some basic styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
background: #f4f4f4;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header styles */
|
||||||
|
header {
|
||||||
|
background-color: #2c3e50;
|
||||||
|
padding: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar input {
|
||||||
|
padding: 10px;
|
||||||
|
width: 250px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar button {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: none;
|
||||||
|
border-radius: 5px;
|
||||||
|
background-color: #3498db;
|
||||||
|
color: white;
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions {
|
||||||
|
background: #363251;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
position: absolute;
|
||||||
|
top: 110%; /* Just below the input */
|
||||||
|
left: 48%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
max-width: fit-content;
|
||||||
|
min-width: 200px; /* Optional: minimum size */
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions div {
|
||||||
|
padding: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions div:hover {
|
||||||
|
background-color: #95a1a8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom-logo {
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
padding: 6px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 0 8px rgba(0, 0, 0, 0.1);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Map container */
|
||||||
|
#map {
|
||||||
|
flex-grow: 1;
|
||||||
|
height: 80vh;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main container */
|
||||||
|
main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
3
run_ora_map.sh
Normal file
3
run_ora_map.sh
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
npm init -y
|
||||||
|
npm install express
|
||||||
|
node server.js
|
||||||
19
server.js
Normal file
19
server.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
const express = require('express');
|
||||||
|
const app = express();
|
||||||
|
const families = require('./data/families.json');
|
||||||
|
|
||||||
|
app.use(express.static('public'));
|
||||||
|
|
||||||
|
app.get('/search', (req, res) => {
|
||||||
|
const query = req.query.family?.toLowerCase();
|
||||||
|
if (!query) {
|
||||||
|
return res.json([]);
|
||||||
|
}
|
||||||
|
const matches = families.filter(fam => fam.family.toLowerCase().includes(query));
|
||||||
|
res.json(matches);
|
||||||
|
});
|
||||||
|
|
||||||
|
const port = 3000;
|
||||||
|
app.listen(port, () => {
|
||||||
|
console.log(`Server running at http://localhost:${port}`);
|
||||||
|
});
|
||||||
Loading…
x
Reference in New Issue
Block a user