Initial commit: AIS Map Android application

This commit is contained in:
ОС Программист
2025-09-02 15:58:16 +03:00
commit 629b403dd2
78 changed files with 9209 additions and 0 deletions
@@ -0,0 +1,442 @@
package com.grigowashere.aismap.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.util.AttributeSet;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
public abstract class BaseDockWidget extends FrameLayout {
private static final String TAG = "BaseDockWidget";
// Константы
protected static final int CIRCLE_SIZE_DP = 120;
protected static final int DEFAULT_DOCK_HEIGHT_DP = 80;
protected static final float MIN_SCALE = 0.5f;
protected static final float MAX_SCALE = 2.0f;
protected static final float SCALE_STEP = 0.1f;
// Состояние виджета
protected boolean isDocked = true; // По умолчанию в dock-режиме
protected boolean dockTop = true;
protected boolean isMorphing = false;
protected float morphProgress = 0.0f; // 0 = dock, 1 = circle
// Перетаскивание
protected boolean dragging = false;
protected float dX, dY;
protected Float targetDragX = null;
protected Float targetDragY = null;
// Изменение размера в dock режиме
protected boolean resizingDock = false;
protected float lastTouchY;
protected int dockHeightPx = 0;
// Масштабирование
protected float scaleFactor = 1.0f;
protected float initialDistance = 0;
protected float initialScale = 1.0f;
// Анимация
protected ValueAnimator morphAnimator;
// Интерфейс для уведомления об изменении размера
public interface OnDockResizeListener {
void onDockResize(int newHeight);
}
protected OnDockResizeListener dockResizeListener;
public BaseDockWidget(Context context) {
super(context);
init();
}
public BaseDockWidget(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
setClickable(true);
setFocusable(true);
// Инициализируем в dock-режиме
post(() -> {
if (isDocked) {
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
setX(0);
setY(0);
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = ViewGroup.LayoutParams.MATCH_PARENT;
lp.height = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
dockHeightPx = 0; // Сбрасываем сохраненную высоту
setLayoutParams(lp);
}
}
});
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (isMorphing) return true;
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
return handleTouchDown(event);
case MotionEvent.ACTION_MOVE:
return handleTouchMove(event);
case MotionEvent.ACTION_UP:
return handleTouchUp(event);
case MotionEvent.ACTION_POINTER_DOWN:
return handlePointerDown(event);
case MotionEvent.ACTION_POINTER_UP:
return handlePointerUp(event);
}
return super.onTouchEvent(event);
}
private boolean handleTouchDown(MotionEvent event) {
float x = event.getX();
float y = event.getY();
if (isDocked) {
// Проверяем зоны изменения размера в зависимости от позиции закрепления
if (dockTop) {
// Если закреплен сверху, зона изменения размера только снизу
if (y > getHeight() - dp(24)) {
resizingDock = true;
lastTouchY = event.getRawY();
return true;
}
} else {
// Если закреплен снизу, зона изменения размера только сверху
if (y < dp(24)) {
resizingDock = true;
lastTouchY = event.getRawY();
return true;
}
}
// Если нажали в центральной области dock-виджета, переводим в movable режим
// Вычисляем новую позицию, чтобы виджет был под пальцем
float newX = event.getRawX() - dp(CIRCLE_SIZE_DP) / 2;
float newY = event.getRawY() - dp(CIRCLE_SIZE_DP) / 2;
// Ограничиваем в пределах экрана
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
newX = Math.max(0, Math.min(newX, parent.getWidth() - dp(CIRCLE_SIZE_DP)));
newY = Math.max(0, Math.min(newY, parent.getHeight() - dp(CIRCLE_SIZE_DP)));
}
setDocked(false, dockTop, newX, newY);
}
// Обычное перетаскивание
dragging = true;
dX = getX() - event.getRawX();
dY = getY() - event.getRawY();
return true;
}
private boolean handleTouchMove(MotionEvent event) {
if (resizingDock) {
handleDockResize(event);
return true;
}
// Обработка масштабирования двумя пальцами
if (event.getPointerCount() == 2 && initialDistance > 0) {
float distance = getDistance(event);
float scale = distance / initialDistance;
scaleFactor = Math.max(MIN_SCALE, Math.min(MAX_SCALE, initialScale * scale));
requestLayout();
return true;
}
if (dragging) {
float newX = event.getRawX() + dX;
float newY = event.getRawY() + dY;
// Ограничиваем движение в пределах родителя
ViewGroup parent = (ViewGroup) getParent();
if (parent != null) {
newX = Math.max(0, Math.min(newX, parent.getWidth() - getWidth()));
newY = Math.max(0, Math.min(newY, parent.getHeight() - getHeight()));
}
setX(newX);
setY(newY);
// Проверяем возможность докинга
checkDocking(event);
return true;
}
return false;
}
private boolean handleTouchUp(MotionEvent event) {
if (resizingDock) {
resizingDock = false;
return true;
}
if (dragging) {
dragging = false;
// Если виджет находится в зоне докинга, доким его
if (shouldDock(event)) {
performDocking(event);
}
return true;
}
return false;
}
private boolean handlePointerDown(MotionEvent event) {
if (event.getPointerCount() == 2) {
initialDistance = getDistance(event);
initialScale = scaleFactor;
}
return true;
}
private boolean handlePointerUp(MotionEvent event) {
if (event.getPointerCount() < 2) {
initialDistance = 0;
}
return true;
}
private void handleDockResize(MotionEvent event) {
float deltaY = event.getRawY() - lastTouchY;
lastTouchY = event.getRawY();
ViewGroup.LayoutParams lp = getLayoutParams();
int newHeight = lp.height;
// Направление изменения размера зависит от позиции закрепления
if (dockTop) {
// Если закреплен сверху, увеличиваем размер при движении вниз
newHeight += (int) deltaY;
} else {
// Если закреплен снизу, увеличиваем размер при движении вверх
newHeight -= (int) deltaY;
}
// Ограничиваем минимальную и максимальную высоту
int minHeight = (int) dp(40);
int maxHeight = ((ViewGroup) getParent()).getHeight() / 2;
newHeight = Math.max(minHeight, Math.min(newHeight, maxHeight));
if (newHeight != lp.height) {
lp.height = newHeight;
dockHeightPx = newHeight;
setLayoutParams(lp);
// Если закреплен снизу, нужно также изменить позицию Y
if (!dockTop) {
float newY = ((ViewGroup) getParent()).getHeight() - newHeight;
setY(newY);
}
if (dockResizeListener != null) {
dockResizeListener.onDockResize(newHeight);
}
}
}
private void checkDocking(MotionEvent event) {
// Проверяем расстояние до краев экрана
float screenHeight = ((ViewGroup) getParent()).getHeight();
float y = event.getRawY();
float dockThreshold = dp(100);
if (y < dockThreshold) {
// Близко к верхнему краю
targetDragX = 0f;
targetDragY = 0f;
} else if (y > screenHeight - dockThreshold) {
// Близко к нижнему краю
targetDragX = 0f;
targetDragY = screenHeight - getHeight();
} else {
targetDragX = null;
targetDragY = null;
}
}
private boolean shouldDock(MotionEvent event) {
return targetDragX != null && targetDragY != null;
}
private void performDocking(MotionEvent event) {
float screenHeight = ((ViewGroup) getParent()).getHeight();
float y = event.getRawY();
boolean dockToTop = y < screenHeight / 2;
// При докинге всегда устанавливаем размер по умолчанию
dockHeightPx = 0; // Сбрасываем сохраненную высоту
setDocked(true, dockToTop, 0f, dockToTop ? 0f : screenHeight - dp(DEFAULT_DOCK_HEIGHT_DP));
}
private float getDistance(MotionEvent event) {
if (event.getPointerCount() < 2) return 0;
float x = event.getX(0) - event.getX(1);
float y = event.getY(0) - event.getY(1);
return (float) Math.sqrt(x * x + y * y);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (isDocked) {
int width = MeasureSpec.getSize(widthMeasureSpec);
int height = dockHeightPx > 0 ? dockHeightPx : (int) dp(DEFAULT_DOCK_HEIGHT_DP);
setMeasuredDimension(width, height);
} else {
int size = (int)(dp(CIRCLE_SIZE_DP) * scaleFactor);
setMeasuredDimension(size, size);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
// Вызываем соответствующий метод отрисовки
if (isDocked) {
onDrawDock(canvas);
} else {
onDrawCircle(canvas);
}
}
public void setDocked(boolean docked, boolean top) {
setDocked(docked, top, getX(), getY());
}
public void setDocked(boolean docked, boolean top, float targetX, float targetY) {
if (this.isDocked == docked && this.dockTop == top && getX() == targetX && getY() == targetY) {
return;
}
this.dockTop = top;
if (morphAnimator != null && morphAnimator.isRunning()) {
morphAnimator.cancel();
}
float startMorph = morphProgress;
float endMorph = docked ? 0f : 1f;
int startW = getWidth();
int startH = getHeight();
ViewGroup parent = (ViewGroup) getParent();
int parentWidth = parent.getWidth();
int parentHeight = parent.getHeight();
int dockHeight = (int) dp(DEFAULT_DOCK_HEIGHT_DP);
int circleSize = (int) dp(CIRCLE_SIZE_DP);
int endW = docked ? parentWidth : circleSize;
int endH = docked ? dockHeight : circleSize;
float startX = getX();
float startY = getY();
float endX = targetX;
float endY = targetY;
// Если доким в нижнюю часть, корректируем позицию Y
if (docked && !top) {
endY = parentHeight - dockHeight;
}
// Сохраняем финальные значения для использования в lambda и inner class
final float finalStartX = startX;
final float finalStartY = startY;
final float finalEndX = endX;
final float finalEndY = endY;
morphAnimator = ValueAnimator.ofFloat(0f, 1f);
morphAnimator.setDuration(350);
morphAnimator.addUpdateListener(anim -> {
float t = (float) anim.getAnimatedValue();
morphProgress = startMorph + (endMorph - startMorph) * t;
int w = (int) (startW + (endW - startW) * t);
int h = (int) (startH + (endH - startH) * t);
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = w;
lp.height = h;
setLayoutParams(lp);
setX(finalStartX + (finalEndX - finalStartX) * t);
setY(finalStartY + (finalEndY - finalStartY) * t);
postInvalidateOnAnimation();
});
morphAnimator.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationEnd(Animator animation) {
ViewGroup.LayoutParams lp = getLayoutParams();
lp.width = endW;
lp.height = endH;
setLayoutParams(lp);
setX(finalEndX);
setY(finalEndY);
morphProgress = endMorph;
postInvalidateOnAnimation();
isMorphing = false;
}
});
morphAnimator.start();
this.isDocked = docked;
isMorphing = true;
}
public boolean isDocked() {
return isDocked;
}
public boolean isDockTop() {
return dockTop;
}
protected boolean isMorphing() {
return isMorphing;
}
public void setOnDockResizeListener(OnDockResizeListener listener) {
this.dockResizeListener = listener;
}
protected float dp(float dp) {
return dp * getResources().getDisplayMetrics().density;
}
// Абстрактные методы для переопределения в наследниках
protected abstract void onDrawDock(Canvas canvas);
protected abstract void onDrawCircle(Canvas canvas);
}
@@ -0,0 +1,334 @@
package com.grigowashere.aismap.view;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.ValueAnimator;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ViewGroup;
import com.grigowashere.aismap.models.AISVessel;
import com.grigowashere.aismap.models.Vessel;
import com.grigowashere.aismap.utils.GeoUtils;
import java.util.ArrayList;
import java.util.List;
public class CompassView extends BaseDockWidget {
private static final String TAG = "CompassView";
private float targetAzimuth = 0;
private float currentAzimuth = 0;
private float magneticCompass = 0; // магнитный компас
private final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint vesselPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path vesselPath = new Path();
private final String[] directions = {"N", "NE", "E", "SE", "S", "SW", "W", "NW"};
private float centerX;
private float centerY;
private static final float SMOOTHING_FACTOR = 0.15f;
private List<AISVessel> nearbyVessels = new ArrayList<>();
private Vessel ourVessel; // наше судно для расчета расстояний
private static final float MAX_DISPLAY_DISTANCE = 10000; // 10 км
private static final float MIN_VESSEL_SIZE = 10; // минимальный размер значка
private static final float MAX_VESSEL_SIZE = 30; // максимальный размер значка
public CompassView(Context context) {
super(context);
init();
}
public CompassView(Context context, AttributeSet attrs) {
super(context, attrs);
init();
}
private void init() {
paint.setColor(Color.WHITE);
paint.setTextAlign(Paint.Align.CENTER);
paint.setTextSize(36f);
vesselPaint.setStyle(Paint.Style.FILL);
vesselPaint.setAntiAlias(true);
// Устанавливаем фон для видимости
setBackgroundColor(Color.argb(200, 0, 0, 0));
}
@Override
protected void onSizeChanged(int w, int h, int oldw, int oldh) {
super.onSizeChanged(w, h, oldw, oldh);
centerX = w / 2f;
centerY = h / 2f;
}
private float getShortestRotation(float start, float end) {
float diff = end - start;
while (diff > 180) diff -= 360;
while (diff < -180) diff += 360;
return diff;
}
// Прямая шкала (dock-режим)
@Override
protected void onDrawDock(Canvas canvas) {
Log.d(TAG, "onDrawDock called, width=" + getWidth() + ", height=" + getHeight());
float w = getWidth();
float h = getHeight();
if (w <= 0 || h <= 0) {
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
return;
}
// Простой фон для начала
paint.setColor(Color.argb(200, 0, 0, 0));
canvas.drawRect(0, 0, w, h, paint);
// Масштабируем размеры в зависимости от высоты виджета
float baseHeight = dp(80); // базовая высота
float scaleFactor = Math.max(0.8f, Math.min(2.0f, h / baseHeight));
// Простой текст для проверки
paint.setColor(Color.WHITE);
paint.setTextSize(24 * scaleFactor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText("КОМПАС", w/2, h/2, paint);
canvas.drawText("Азимут: " + (int)currentAzimuth + "°", w/2, h/2 + 30 * scaleFactor, paint);
canvas.drawText("Магн: " + (int)magneticCompass + "°", w/2, h/2 + 60 * scaleFactor, paint);
// Плавное обновление азимута
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
if (Math.abs(diff) > 0.1f) {
currentAzimuth += diff * SMOOTHING_FACTOR;
if (currentAzimuth > 360) currentAzimuth -= 360;
if (currentAzimuth < 0) currentAzimuth += 360;
postInvalidateOnAnimation();
}
// Рисуем простую шкалу
float centerX = w / 2f;
float centerY = h / 2f;
float visibleDegrees = 120;
// Рисуем деления шкалы
for (int degree = 0; degree < 360; degree += 15) {
// Вычисляем относительное положение деления
float relativeDegree = (degree - currentAzimuth + 360) % 360;
if (relativeDegree > 180) relativeDegree -= 360;
// Рисуем только видимые деления
if (Math.abs(relativeDegree) <= visibleDegrees / 2) {
float x = centerX + (relativeDegree / (visibleDegrees / 2)) * (w / 2);
float lineHeight = (degree % 30 == 0) ? 20 * scaleFactor : 10 * scaleFactor;
canvas.drawLine(x, centerY - lineHeight, x, centerY + lineHeight, paint);
if (degree % 30 == 0) {
String degreeText = String.valueOf(degree);
paint.setTextSize(16 * scaleFactor);
canvas.drawText(degreeText, x, centerY - 30 * scaleFactor, paint);
}
if (degree % 45 == 0) {
int directionIndex = (degree / 45) % 8;
if (directionIndex < directions.length) {
paint.setTextSize(18 * scaleFactor);
canvas.drawText(directions[directionIndex], x, centerY + 50 * scaleFactor, paint);
}
}
}
}
// Рисуем суда
for (AISVessel vessel : nearbyVessels) {
float relativeBearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
if (relativeBearing > 180) relativeBearing -= 360;
if (Math.abs(relativeBearing) <= visibleDegrees / 2) {
float x = centerX + (relativeBearing / (visibleDegrees / 2)) * (w / 2);
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
float size = calculateVesselSize((float) distance) * scaleFactor;
vesselPaint.setColor(getVesselColor(vessel));
drawVesselTriangle(canvas, x, centerY, size, (float) (vessel.getCourse() - currentAzimuth));
}
}
// Центральная линия (направление вперёд)
paint.setColor(Color.RED);
paint.setStrokeWidth(3 * scaleFactor);
canvas.drawLine(centerX, centerY - h/2, centerX, centerY + h/2, paint);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(1);
// Выделяем зону resize в зависимости от позиции закрепления
if (isDocked) {
Paint resizePaint = new Paint(Paint.ANTI_ALIAS_FLAG);
resizePaint.setColor(Color.argb(120, 255, 255, 255));
resizePaint.setStyle(Paint.Style.STROKE);
resizePaint.setStrokeWidth(2);
paint.setTextSize(12);
paint.setColor(Color.WHITE);
if (isDockTop()) {
// Если закреплен сверху, показываем зону resize снизу
canvas.drawRect(0, h - dp(24), w, h, resizePaint);
canvas.drawText("", w/2, h - dp(12), paint);
} else {
// Если закреплен снизу, показываем зону resize сверху
canvas.drawRect(0, 0, w, dp(24), resizePaint);
canvas.drawText("", w/2, dp(12), paint);
}
}
}
// Круглый компас (draggable-режим)
@Override
protected void onDrawCircle(Canvas canvas) {
Log.d(TAG, "onDrawCircle called, width=" + getWidth() + ", height=" + getHeight());
float w = getWidth();
float h = getHeight();
if (w <= 0 || h <= 0) {
Log.w(TAG, "Invalid dimensions: width=" + w + ", height=" + h);
return;
}
float cx = w / 2f;
float cy = h / 2f;
float radius = Math.min(w, h) / 2f * 0.9f;
// Масштабируем размеры в зависимости от размера виджета
float baseSize = dp(120); // базовая высота
float scaleFactor = Math.max(0.8f, Math.min(2.0f, Math.min(w, h) / baseSize));
// Фон
paint.setColor(Color.argb(200, 0, 0, 0));
canvas.drawCircle(cx, cy, radius, paint);
paint.setColor(Color.WHITE);
// Плавное обновление азимута
float diff = getShortestRotation(currentAzimuth, targetAzimuth);
if (Math.abs(diff) > 0.1f) {
currentAzimuth += diff * SMOOTHING_FACTOR;
if (currentAzimuth > 360) currentAzimuth -= 360;
if (currentAzimuth < 0) currentAzimuth += 360;
postInvalidateOnAnimation();
}
// Деления и метки по кругу
for (int degree = 0; degree < 360; degree += 30) {
float angle = (float) Math.toRadians(degree - currentAzimuth);
float x1 = cx + (float) Math.sin(angle) * (radius * 0.85f);
float y1 = cy - (float) Math.cos(angle) * (radius * 0.85f);
float x2 = cx + (float) Math.sin(angle) * radius;
float y2 = cy - (float) Math.cos(angle) * radius;
paint.setStrokeWidth(2 * scaleFactor);
canvas.drawLine(x1, y1, x2, y2, paint);
if (degree % 90 == 0) {
int directionIndex = (degree / 90) % 4;
String[] mainDirections = {"N", "E", "S", "W"};
float dx = cx + (float) Math.sin(angle) * (radius - 25 * scaleFactor);
float dy = cy - (float) Math.cos(angle) * (radius - 25 * scaleFactor);
paint.setTextSize(16 * scaleFactor);
canvas.drawText(mainDirections[directionIndex], dx, dy, paint);
}
}
// Рисуем суда по кругу
for (AISVessel vessel : nearbyVessels) {
float bearing = (float) ((vessel.getCourse() - currentAzimuth + 360) % 360);
float angle = (float) Math.toRadians(bearing);
float vesselRadius = radius * 0.6f;
float vx = cx + (float) Math.sin(angle) * vesselRadius;
float vy = cy - (float) Math.cos(angle) * vesselRadius;
double distance = ourVessel != null ? GeoUtils.calculateDistance(ourVessel, vessel) : 0;
float size = calculateVesselSize((float) distance) * scaleFactor;
vesselPaint.setColor(getVesselColor(vessel));
drawVesselTriangle(canvas, vx, vy, size, (float) (vessel.getCourse() - currentAzimuth));
}
// Центральная линия (направление вперёд)
paint.setColor(Color.RED);
paint.setStrokeWidth(3 * scaleFactor);
canvas.drawLine(cx, cy, cx, cy - radius, paint);
paint.setColor(Color.WHITE);
paint.setStrokeWidth(1);
// Текст азимута в центре
paint.setTextSize(14 * scaleFactor);
paint.setTextAlign(Paint.Align.CENTER);
canvas.drawText((int)currentAzimuth + "°", cx, cy + 5 * scaleFactor, paint);
}
private float calculateVesselSize(float distance) {
if (distance > MAX_DISPLAY_DISTANCE) return MIN_VESSEL_SIZE;
// Линейная интерполяция размера от расстояния
float ratio = 1 - Math.min(distance / MAX_DISPLAY_DISTANCE, 1);
return MIN_VESSEL_SIZE + (MAX_VESSEL_SIZE - MIN_VESSEL_SIZE) * ratio;
}
private int getVesselColor(AISVessel vessel) {
// Можно настроить цвета в зависимости от параметров судна
// Используем navigation status из AIS данных
int navStatus = GeoUtils.getNavigationStatusCode(vessel.getNavigationalStatus());
switch (navStatus) {
case 0: // Under way using engine
return Color.GREEN;
case 1: // At anchor
return Color.YELLOW;
case 5: // Moored
return Color.BLUE;
default:
return Color.WHITE;
}
}
private void drawVesselTriangle(Canvas canvas, float x, float y, float size, float rotation) {
vesselPath.reset();
// Создаем треугольник
float halfSize = size / 2;
vesselPath.moveTo(x, y - halfSize); // вершина
vesselPath.lineTo(x - halfSize, y + halfSize); // левый нижний угол
vesselPath.lineTo(x + halfSize, y + halfSize); // правый нижний угол
vesselPath.close();
// Поворачиваем треугольник
canvas.save();
canvas.rotate(rotation, x, y);
canvas.drawPath(vesselPath, vesselPaint);
canvas.restore();
}
public void setAzimuth(float azimuth) {
this.targetAzimuth = azimuth;
invalidate();
}
public void setMagneticCompass(float magneticCompass) {
this.magneticCompass = magneticCompass;
invalidate();
}
public void updateNearbyVessels(List<AISVessel> vessels) {
this.nearbyVessels = vessels;
invalidate();
}
public void setOurVessel(Vessel ourVessel) {
this.ourVessel = ourVessel;
invalidate();
}
}